Skip to content

Commit 4664f61

Browse files
authored
Merge pull request continuedev#4742 from continuedev/dallin/mcp-fixes
Tool/MCP Improvements
2 parents 71f5dda + 5e8eec3 commit 4664f61

File tree

35 files changed

+978
-463
lines changed

35 files changed

+978
-463
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,
@@ -521,6 +520,8 @@ async function intermediateToFinalConfig(
521520
contextProviders,
522521
models,
523522
tools: allTools,
523+
mcpServerStatuses: [],
524+
slashCommands: config.slashCommands ?? [],
524525
modelsByRole: {
525526
chat: models,
526527
edit: models,
@@ -541,53 +542,18 @@ async function intermediateToFinalConfig(
541542
},
542543
};
543544

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

592558
// Handle experimental modelRole config values for apply and edit
593559
const inlineEditModel = getModelByRole(continueConfig, "inlineEdit")?.title;
@@ -681,6 +647,7 @@ async function finalToBrowserConfig(
681647
rules: final.rules,
682648
docs: final.docs,
683649
tools: final.tools,
650+
mcpServerStatuses: final.mcpServerStatuses,
684651
tabAutocompleteOptions: final.tabAutocompleteOptions,
685652
usePlatform: await useHub(ide.getIdeSettings()),
686653
modelsByRole: Object.fromEntries(

core/config/profile/doLoadConfig.ts

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

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

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

core/config/yaml/loadYaml.ts

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

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

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

0 commit comments

Comments
 (0)