Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions extensions/cli/src/services/ConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { logger } from "../util/logger.js";

import { BaseService, ServiceWithDependencies } from "./BaseService.js";
import { loadJsonMcpServers } from "./loadJsonMcpServers.js";
import { serviceContainer } from "./ServiceContainer.js";
import { ToolPermissionServiceState } from "./ToolPermissionService.js";
import {
Expand Down Expand Up @@ -96,6 +97,7 @@ export class ConfigService
packageIdentifiers,
additional,
);
this.processJsonMcpServers(additional);
this.processAgentFileRules(agentFileState, packageIdentifiers);
this.processRulesAndPrompts(
options.rule || [],
Expand Down Expand Up @@ -157,6 +159,12 @@ export class ConfigService
}
}

private processJsonMcpServers(additional: AssistantUnrolled): void {
for (const server of loadJsonMcpServers()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Auto-discovered MCP servers should be filtered against servers already present in additional.mcpServers before pushing, and the precedence relative to the loaded assistant config should be explicit. Currently the JSON discovery path appends blindly, relying on downstream merge order and risking silent override of config-defined servers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/services/ConfigService.ts, line 163:

<comment>Auto-discovered MCP servers should be filtered against servers already present in `additional.mcpServers` before pushing, and the precedence relative to the loaded assistant config should be explicit. Currently the JSON discovery path appends blindly, relying on downstream merge order and risking silent override of config-defined servers.</comment>

<file context>
@@ -157,6 +159,12 @@ export class ConfigService
   }
 
+  private processJsonMcpServers(additional: AssistantUnrolled): void {
+    for (const server of loadJsonMcpServers()) {
+      additional.mcpServers!.push(server);
+    }
</file context>

additional.mcpServers!.push(server);
}
}
Comment on lines +162 to +166

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new MCP JSON discovery hook is not guarded inside ConfigService, so an unexpected exception from loadJsonMcpServers can abort config loading, startup, switchConfig, or reload instead of being treated as an optional discovery failure with a warning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/services/ConfigService.ts, line 162:

<comment>The new MCP JSON discovery hook is not guarded inside ConfigService, so an unexpected exception from `loadJsonMcpServers` can abort config loading, startup, switchConfig, or reload instead of being treated as an optional discovery failure with a warning.</comment>

<file context>
@@ -157,6 +159,12 @@ export class ConfigService
     }
   }
 
+  private processJsonMcpServers(additional: AssistantUnrolled): void {
+    for (const server of loadJsonMcpServers()) {
+      additional.mcpServers!.push(server);
</file context>
Suggested change
private processJsonMcpServers(additional: AssistantUnrolled): void {
for (const server of loadJsonMcpServers()) {
additional.mcpServers!.push(server);
}
}
private processJsonMcpServers(additional: AssistantUnrolled): void {
try {
for (const server of loadJsonMcpServers()) {
additional.mcpServers!.push(server);
}
} catch (e) {
logger.warn(`Failed to load JSON MCP servers: ${getErrorString(e)}`);
}
}


private processAgentFileRules(
agentFileState: AgentFileServiceState | undefined,
packageIdentifiers: PackageIdentifier[],
Expand Down
91 changes: 91 additions & 0 deletions extensions/cli/src/services/loadJsonMcpServers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { loadJsonMcpServers } from "./loadJsonMcpServers.js";

describe("loadJsonMcpServers", () => {
let cwd: string;

beforeEach(() => {
cwd = fs.mkdtempSync(path.join(os.tmpdir(), "cn-mcp-json-"));
});

afterEach(() => {
fs.rmSync(cwd, { recursive: true, force: true });
});

function writeWorkspaceMcpFile(fileName: string, content: unknown): void {
const dir = path.join(cwd, ".continue", "mcpServers");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, fileName), JSON.stringify(content));
}

it("returns an empty array when there is no mcpServers directory", () => {
expect(loadJsonMcpServers(cwd)).toEqual([]);
});

it("discovers a single-server stdio file, named after the file", () => {
writeWorkspaceMcpFile("playwright.json", {
command: "npx",
args: ["-y", "@playwright/mcp"],
});

const servers = loadJsonMcpServers(cwd);

expect(servers).toHaveLength(1);
expect(servers[0]).toMatchObject({
name: "playwright",
type: "stdio",
command: "npx",
args: ["-y", "@playwright/mcp"],
});
});

it("discovers servers from a Claude-desktop-style mcpServers map", () => {
writeWorkspaceMcpFile("config.json", {
mcpServers: { weather: { command: "uvx", args: ["weather-mcp"] } },
});

const servers = loadJsonMcpServers(cwd);

expect(servers.map((s) => s.name)).toEqual(["weather"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Weak assertion in Claude-desktop test: only checks server name, does not verify command/args properties are correctly parsed through convertJsonMcpConfigToYamlMcpConfig

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/services/loadJsonMcpServers.test.ts, line 54:

<comment>Weak assertion in Claude-desktop test: only checks server name, does not verify command/args properties are correctly parsed through convertJsonMcpConfigToYamlMcpConfig</comment>

<file context>
@@ -0,0 +1,91 @@
+
+    const servers = loadJsonMcpServers(cwd);
+
+    expect(servers.map((s) => s.name)).toEqual(["weather"]);
+  });
+
</file context>

});

it("discovers an http server and maps the transport type", () => {
writeWorkspaceMcpFile("remote.json", {
url: "https://example.com/mcp",
type: "http",
});

const servers = loadJsonMcpServers(cwd);

expect(servers[0]).toMatchObject({
name: "remote",
url: "https://example.com/mcp",
type: "streamable-http",
});
});

it("skips files that do not match a supported MCP config format", () => {
writeWorkspaceMcpFile("notes.json", { hello: "world" });

expect(loadJsonMcpServers(cwd)).toEqual([]);
});

it("de-duplicates servers by name, keeping the first encountered", () => {
writeWorkspaceMcpFile("a.json", {
mcpServers: { dup: { command: "first" } },
});
writeWorkspaceMcpFile("b.json", {
mcpServers: { dup: { command: "second" } },
});

const servers = loadJsonMcpServers(cwd);

expect(servers).toHaveLength(1);
expect(servers[0]).toMatchObject({ name: "dup", command: "first" });
});
});
134 changes: 134 additions & 0 deletions extensions/cli/src/services/loadJsonMcpServers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as fs from "fs";
import * as path from "path";

import {
claudeCodeLikeConfigFileSchema,
claudeDesktopLikeConfigFileSchema,
convertJsonMcpConfigToYamlMcpConfig,
type McpJsonConfig,
mcpServersJsonSchema,
} from "@continuedev/config-yaml";

import { env } from "../env.js";
import { getErrorString } from "../util/error.js";
import { logger } from "../util/logger.js";

import { MCPServerConfig } from "./types.js";

const MCP_SERVERS_DIRNAME = "mcpServers";

interface NamedMcpJsonConfig {
name: string;
mcpJson: McpJsonConfig;
}

// Workspace `.continue/mcpServers` and the global `~/.continue/mcpServers`,
// matching where the IDE extensions look for standalone MCP server files.
function getMcpServerDirs(cwd: string): string[] {
return [
path.join(cwd, ".continue", MCP_SERVERS_DIRNAME),
path.join(env.continueHome, MCP_SERVERS_DIRNAME),
];
}

// A file may hold a single server, a Claude-desktop-style `{ mcpServers: {...} }`
// map, or a Claude-code-style file with top-level and per-project servers.
function parseMcpServerFile(
content: string,
fileName: string,
): NamedMcpJsonConfig[] {
const json = JSON.parse(content);

const claudeCode = claudeCodeLikeConfigFileSchema.safeParse(json);
if (claudeCode.success) {
const records = [
claudeCode.data.mcpServers,
...Object.values(claudeCode.data.projects).map((p) => p.mcpServers),
];
return records
.filter((record): record is NonNullable<typeof record> => !!record)
.flatMap((record) =>
Object.entries(record).map(([name, mcpJson]) => ({ name, mcpJson })),
);
}

const claudeDesktop = claudeDesktopLikeConfigFileSchema.safeParse(json);
if (claudeDesktop.success) {
return Object.entries(claudeDesktop.data.mcpServers).map(
([name, mcpJson]) => ({ name, mcpJson }),
);
}

const single = mcpServersJsonSchema.safeParse(json);
if (single.success) {
return [{ name: fileName.replace(/\.json$/, ""), mcpJson: single.data }];
}

throw new Error("file does not match a supported MCP JSON config format");
}

/**
* Discover MCP servers defined as standalone JSON files in `.continue/mcpServers`
* (workspace and global), bringing the CLI to parity with the IDE extensions.
* Returns configs in the same shape as the rest of the assistant's mcpServers.
*/
export function loadJsonMcpServers(
cwd: string = process.cwd(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Workspace MCP server discovery defaults to process.cwd(), which may miss .continue/mcpServers/ at the project root when cn is launched from a subdirectory. The IDE equivalent resolves actual workspace directories instead of relying on the shell cwd.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/services/loadJsonMcpServers.ts, line 76:

<comment>Workspace MCP server discovery defaults to `process.cwd()`, which may miss `.continue/mcpServers/` at the project root when `cn` is launched from a subdirectory. The IDE equivalent resolves actual workspace directories instead of relying on the shell cwd.</comment>

<file context>
@@ -0,0 +1,134 @@
+ * Returns configs in the same shape as the rest of the assistant's mcpServers.
+ */
+export function loadJsonMcpServers(
+  cwd: string = process.cwd(),
+): MCPServerConfig[] {
+  const servers: MCPServerConfig[] = [];
</file context>

): MCPServerConfig[] {
const servers: MCPServerConfig[] = [];
const seenNames = new Set<string>();

for (const dir of getMcpServerDirs(cwd)) {
let fileNames: string[];
try {
if (!fs.existsSync(dir)) {
continue;
}
fileNames = fs
.readdirSync(dir)
.filter((fileName) => fileName.endsWith(".json"))
.sort();
} catch (e) {
logger.warn(
`Failed to read MCP servers directory ${dir}: ${getErrorString(e)}`,
);
continue;
}

for (const fileName of fileNames) {
const filePath = path.join(dir, fileName);
let parsed: NamedMcpJsonConfig[];
try {
parsed = parseMcpServerFile(
fs.readFileSync(filePath, "utf-8"),
fileName,
);
} catch (e) {
logger.warn(
`Skipping invalid MCP server file ${filePath}: ${getErrorString(e)}`,
);
continue;
}

for (const { name, mcpJson } of parsed) {
if (seenNames.has(name)) {
continue;
}
try {
const { yamlConfig } = convertJsonMcpConfigToYamlMcpConfig(
name,
mcpJson,
);
servers.push(yamlConfig as MCPServerConfig);
seenNames.add(name);
} catch (e) {
logger.warn(
`Failed to load MCP server "${name}" from ${filePath}: ${getErrorString(e)}`,
);
}
}
}
}

return servers;
}
Loading