Skip to content

Commit 61ea04b

Browse files
committed
Merge branch 'main' into pe/conversation-starters
2 parents 2f199ff + 716f12e commit 61ea04b

File tree

37 files changed

+995
-480
lines changed

37 files changed

+995
-480
lines changed

core/config/load.ts

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
IdeType,
2929
ILLM,
3030
LLMOptions,
31-
MCPOptions,
3231
ModelDescription,
3332
RerankerDescription,
3433
SerializedContinueConfig,
@@ -518,6 +517,8 @@ async function intermediateToFinalConfig(
518517
contextProviders,
519518
models,
520519
tools: allTools,
520+
mcpServerStatuses: [],
521+
slashCommands: config.slashCommands ?? [],
521522
modelsByRole: {
522523
chat: models,
523524
edit: models,
@@ -538,53 +539,18 @@ async function intermediateToFinalConfig(
538539
},
539540
};
540541

541-
// Apply MCP if specified
542+
// Trigger MCP server refreshes (Config is reloaded again once connected!)
542543
const mcpManager = MCPManagerSingleton.getInstance();
543-
function getMcpId(options: MCPOptions) {
544-
return JSON.stringify(options);
545-
}
546-
if (config.experimental?.modelContextProtocolServers) {
547-
await mcpManager.removeUnusedConnections(
548-
config.experimental.modelContextProtocolServers.map(getMcpId),
549-
);
550-
}
551-
552-
if (config.experimental?.modelContextProtocolServers) {
553-
const abortController = new AbortController();
554-
const mcpConnectionTimeout = setTimeout(
555-
() => abortController.abort(),
556-
5000,
557-
);
558-
559-
await Promise.allSettled(
560-
config.experimental.modelContextProtocolServers?.map(
561-
async (server, index) => {
562-
try {
563-
const mcpId = getMcpId(server);
564-
const mcpConnection = mcpManager.createConnection(mcpId, server);
565-
await mcpConnection.modifyConfig(
566-
continueConfig,
567-
mcpId,
568-
abortController.signal,
569-
"MCP Server",
570-
server.faviconUrl,
571-
);
572-
} catch (e) {
573-
let errorMessage = "Failed to load MCP server";
574-
if (e instanceof Error) {
575-
errorMessage += ": " + e.message;
576-
}
577-
errors.push({
578-
fatal: false,
579-
message: errorMessage,
580-
});
581-
} finally {
582-
clearTimeout(mcpConnectionTimeout);
583-
}
584-
},
585-
) || [],
586-
);
587-
}
544+
mcpManager.setConnections(
545+
(config.experimental?.modelContextProtocolServers ?? []).map(
546+
(server, index) => ({
547+
id: `continue-mcp-server-${index + 1}`,
548+
name: `MCP Server ${index + 1}`,
549+
...server,
550+
}),
551+
),
552+
false,
553+
);
588554

589555
// Handle experimental modelRole config values for apply and edit
590556
const inlineEditModel = getModelByRole(continueConfig, "inlineEdit")?.title;
@@ -676,6 +642,7 @@ async function finalToBrowserConfig(
676642
rules: final.rules,
677643
docs: final.docs,
678644
tools: final.tools,
645+
mcpServerStatuses: final.mcpServerStatuses,
679646
tabAutocompleteOptions: final.tabAutocompleteOptions,
680647
usePlatform: await useHub(ide.getIdeSettings()),
681648
modelsByRole: Object.fromEntries(

core/config/profile/doLoadConfig.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ import {
1313
IDE,
1414
IdeSettings,
1515
SerializedContinueConfig,
16+
Tool,
1617
} from "../../";
18+
import { constructMcpSlashCommand } from "../../commands/slash/mcp";
19+
import { MCPManagerSingleton } from "../../context/mcp";
20+
import MCPContextProvider from "../../context/providers/MCPContextProvider";
1721
import { ControlPlaneProxyInfo } from "../../control-plane/analytics/IAnalyticsProvider.js";
1822
import { ControlPlaneClient } from "../../control-plane/client.js";
1923
import { getControlPlaneEnv } from "../../control-plane/env.js";
2024
import { TeamAnalytics } from "../../control-plane/TeamAnalytics.js";
2125
import ContinueProxy from "../../llm/llms/stubs/ContinueProxy";
26+
import { encodeMCPToolUri } from "../../tools/callTool";
2227
import { getConfigJsonPath, getConfigYamlPath } from "../../util/paths";
2328
import { localPathOrUriToPath } from "../../util/pathToUri";
2429
import { Telemetry } from "../../util/posthog";
@@ -95,15 +100,90 @@ export default async function doLoadConfig(
95100
configLoadInterrupted = result.configLoadInterrupted;
96101
}
97102

98-
// Rectify model selections for each role
99-
if (newConfig) {
100-
newConfig = rectifySelectedModelsFromGlobalContext(newConfig, profileId);
101-
}
102-
103103
if (configLoadInterrupted || !newConfig) {
104104
return { errors, config: newConfig, configLoadInterrupted: true };
105105
}
106106

107+
// TODO using config result but result with non-fatal errors is an antipattern?
108+
// Remove ability have undefined errors, just have an array
109+
errors = [...(errors ?? [])];
110+
111+
// Rectify model selections for each role
112+
newConfig = rectifySelectedModelsFromGlobalContext(newConfig, profileId);
113+
114+
// Add things from MCP servers
115+
const mcpManager = MCPManagerSingleton.getInstance();
116+
const mcpServerStatuses = mcpManager.getStatuses();
117+
118+
// Slightly hacky just need connection's client to make slash command for now
119+
const serializableStatuses = mcpServerStatuses.map((server) => {
120+
const { client, ...rest } = server;
121+
return rest;
122+
});
123+
newConfig.mcpServerStatuses = serializableStatuses;
124+
125+
for (const server of mcpServerStatuses) {
126+
if (server.status === "connected") {
127+
const serverTools: Tool[] = server.tools.map((tool) => ({
128+
displayTitle: server.name + " " + tool.name,
129+
function: {
130+
description: tool.description,
131+
name: tool.name,
132+
parameters: tool.inputSchema,
133+
},
134+
faviconUrl: server.faviconUrl,
135+
readonly: false,
136+
type: "function" as const,
137+
wouldLikeTo: "",
138+
uri: encodeMCPToolUri(server.id, tool.name),
139+
group: server.name,
140+
}));
141+
newConfig.tools.push(...serverTools);
142+
143+
const serverSlashCommands = server.prompts.map((prompt) =>
144+
constructMcpSlashCommand(
145+
server.client,
146+
prompt.name,
147+
prompt.description,
148+
prompt.arguments?.map((a: any) => a.name),
149+
),
150+
);
151+
newConfig.slashCommands.push(...serverSlashCommands);
152+
153+
const submenuItems = server.resources.map((resource) => ({
154+
title: resource.name,
155+
description: resource.description ?? resource.name,
156+
id: resource.uri,
157+
icon: server.faviconUrl,
158+
}));
159+
if (submenuItems.length > 0) {
160+
const serverContextProvider = new MCPContextProvider({
161+
submenuItems,
162+
mcpId: server.id,
163+
});
164+
newConfig.contextProviders.push(serverContextProvider);
165+
}
166+
}
167+
}
168+
169+
// Detect duplicate tool names
170+
const counts: Record<string, number> = {};
171+
newConfig.tools.forEach((tool) => {
172+
if (counts[tool.function.name]) {
173+
counts[tool.function.name] = counts[tool.function.name] + 1;
174+
} else {
175+
counts[tool.function.name] = 1;
176+
}
177+
});
178+
Object.entries(counts).forEach(([toolName, count]) => {
179+
if (count > 1) {
180+
errors!.push({
181+
fatal: false,
182+
message: `Duplicate (${count}) tools named "${toolName}" detected. Permissions will conflict and usage may be unpredictable`,
183+
});
184+
}
185+
});
186+
107187
newConfig.allowAnonymousTelemetry =
108188
newConfig.allowAnonymousTelemetry && (await ide.isTelemetryEnabled());
109189

core/config/yaml/loadYaml.ts

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ async function configYamlToContinueConfig(
110110
const continueConfig: ContinueConfig = {
111111
slashCommands: [],
112112
models: [],
113-
tools: allTools,
113+
tools: [...allTools],
114+
mcpServerStatuses: [],
114115
systemMessage: config.rules?.join("\n"),
115116
experimental: {
116117
modelContextProtocolServers: config.mcpServers?.map((mcpServer) => ({
@@ -366,52 +367,19 @@ async function configYamlToContinueConfig(
366367
continueConfig.contextProviders.push(new DocsContextProvider({}));
367368
}
368369

369-
// Apply MCP if specified
370+
// Trigger MCP server refreshes (Config is reloaded again once connected!)
370371
const mcpManager = MCPManagerSingleton.getInstance();
371-
if (config.mcpServers) {
372-
await mcpManager.removeUnusedConnections(
373-
config.mcpServers.map((s) => s.name),
374-
);
375-
}
376-
377-
await Promise.allSettled(
378-
config.mcpServers?.map(async (server) => {
379-
const abortController = new AbortController();
380-
const mcpConnectionTimeout = setTimeout(
381-
() => abortController.abort(),
382-
5000,
383-
);
384-
385-
try {
386-
const mcpId = server.name;
387-
const mcpConnection = mcpManager.createConnection(mcpId, {
388-
transport: {
389-
type: "stdio",
390-
args: [],
391-
...server,
392-
},
393-
});
394-
395-
await mcpConnection.modifyConfig(
396-
continueConfig,
397-
mcpId,
398-
abortController.signal,
399-
server.name,
400-
server.faviconUrl,
401-
);
402-
} catch (e) {
403-
let errorMessage = `Failed to load MCP server ${server.name}`;
404-
if (e instanceof Error) {
405-
errorMessage += ": " + e.message;
406-
}
407-
localErrors.push({
408-
fatal: false,
409-
message: errorMessage,
410-
});
411-
} finally {
412-
clearTimeout(mcpConnectionTimeout);
413-
}
414-
}) ?? [],
372+
mcpManager.setConnections(
373+
(config.mcpServers ?? []).map((server) => ({
374+
id: server.name,
375+
name: server.name,
376+
transport: {
377+
type: "stdio",
378+
args: [],
379+
...server,
380+
},
381+
})),
382+
false,
415383
);
416384

417385
return { config: continueConfig, errors: localErrors };

0 commit comments

Comments
 (0)