From 7b11faa1502967295382bfd89aa890e64fcbbb71 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 16:15:02 -0400 Subject: [PATCH] fix: discover MCP servers from project-root .mcp.json (#692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mcporter extension only discovered servers that the mcporter CLI itself knew about (via .vscode/mcp.json, Claude Desktop config, etc.). Servers configured in the standard .mcp.json at the project root — used by Claude Code, Cursor, and other AI coding tools — were invisible. Changes: 1. mcporter extension (index.ts): - Add readProjectMcpJson() that reads .mcp.json from cwd and returns servers not already discovered by mcporter - Merge .mcp.json servers into getServerList() results - Add getMcpJsonServerUrl() to resolve HTTP URLs for .mcp.json servers - Update getServerDetail() to pass HTTP URLs directly to mcporter for servers only known via .mcp.json - Update mcp_call to use HTTP URL as server reference for .mcp.json servers 2. discover_configs scanner (scanners.ts): - Add .mcp.json to the project-level MCP config scan path alongside .claude/.mcp.json and .claude/mcp.json Closes #692 --- src/resources/extensions/mcporter/index.ts | 75 ++++++++++++++++++- .../extensions/universal-config/scanners.ts | 4 +- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/resources/extensions/mcporter/index.ts b/src/resources/extensions/mcporter/index.ts index 7cb041b46..f5d2657b3 100644 --- a/src/resources/extensions/mcporter/index.ts +++ b/src/resources/extensions/mcporter/index.ts @@ -26,6 +26,8 @@ import { Text } from "@gsd/pi-tui"; import { Type } from "@sinclair/typebox"; import { execFile, exec } from "node:child_process"; import { promisify } from "node:util"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; const execFileAsync = promisify(execFile); const execAsync = promisify(exec); @@ -99,6 +101,37 @@ async function runMcporter( return stdout; } +/** + * Read .mcp.json from the project root (cwd) and return servers not already + * discovered by mcporter. This bridges the gap where mcporter doesn't scan + * the standard .mcp.json config used by Claude Code, Cursor, etc. + */ +function readProjectMcpJson(knownNames: Set): McpServer[] { + const servers: McpServer[] = []; + try { + const mcpJsonPath = join(process.cwd(), ".mcp.json"); + if (!existsSync(mcpJsonPath)) return servers; + const raw = readFileSync(mcpJsonPath, "utf-8"); + const data = JSON.parse(raw) as Record; + const mcpServers = (data.mcpServers ?? data.servers) as Record> | undefined; + if (!mcpServers || typeof mcpServers !== "object") return servers; + + for (const [name, config] of Object.entries(mcpServers)) { + if (knownNames.has(name)) continue; // Already discovered by mcporter + const transport = (config.type as string) ?? (config.command ? "stdio" : config.url ? "http" : "unknown"); + servers.push({ + name, + status: "ok", + transport, + tools: [], // Tools unknown until mcp_discover is called + }); + } + } catch { + // Non-fatal — .mcp.json may not exist or be malformed + } + return servers; +} + async function getServerList(signal?: AbortSignal): Promise { if (serverListCache) return serverListCache; @@ -112,6 +145,14 @@ async function getServerList(signal?: AbortSignal): Promise { if (!Array.isArray(data.servers)) { throw new Error(`Unexpected mcporter response shape: ${JSON.stringify(Object.keys(data))}`); } + + // Merge servers from project-root .mcp.json that mcporter didn't discover + const knownNames = new Set(data.servers.map((s) => s.name)); + const projectServers = readProjectMcpJson(knownNames); + if (projectServers.length > 0) { + data.servers.push(...projectServers); + } + serverListCache = data.servers; return serverListCache; } @@ -122,12 +163,39 @@ async function getServerDetail( ): Promise { if (serverDetailCache.has(serverName)) return serverDetailCache.get(serverName)!; - const raw = await runMcporter(["list", serverName, "--schema", "--json"], signal); + // Check if this server came from .mcp.json (not known to mcporter natively) + const mcpJsonUrl = getMcpJsonServerUrl(serverName); + const args = mcpJsonUrl + ? ["list", mcpJsonUrl, "--schema", "--json"] + : ["list", serverName, "--schema", "--json"]; + + const raw = await runMcporter(args, signal); const data = JSON.parse(raw) as McpServerDetail; + // Preserve the user-facing name from .mcp.json + if (mcpJsonUrl) data.name = serverName; serverDetailCache.set(serverName, data); return data; } +/** + * Look up a server's URL from .mcp.json if it's an HTTP server not known to mcporter. + */ +function getMcpJsonServerUrl(serverName: string): string | null { + try { + const mcpJsonPath = join(process.cwd(), ".mcp.json"); + if (!existsSync(mcpJsonPath)) return null; + const raw = readFileSync(mcpJsonPath, "utf-8"); + const data = JSON.parse(raw) as Record; + const mcpServers = (data.mcpServers ?? data.servers) as Record> | undefined; + if (!mcpServers?.[serverName]) return null; + const config = mcpServers[serverName]; + if (config.type === "http" && typeof config.url === "string") return config.url; + return null; + } catch { + return null; + } +} + function formatServerList(servers: McpServer[]): string { if (servers.length === 0) return "No MCP servers found."; @@ -328,7 +396,10 @@ export default function (pi: ExtensionAPI) { async execute(_id, params, signal) { // Build mcporter call command: mcporter call server.tool key:value ... - const callTarget = `${params.server}.${params.tool}`; + // For HTTP servers from .mcp.json, use the URL directly as the server identifier + const mcpJsonUrl = getMcpJsonServerUrl(params.server); + const serverRef = mcpJsonUrl ?? params.server; + const callTarget = `${serverRef}.${params.tool}`; const cliArgs = ["call", callTarget, "--output", "raw"]; if (params.args && Object.keys(params.args).length > 0) { diff --git a/src/resources/extensions/universal-config/scanners.ts b/src/resources/extensions/universal-config/scanners.ts index 874a7b0f4..dbb01c303 100644 --- a/src/resources/extensions/universal-config/scanners.ts +++ b/src/resources/extensions/universal-config/scanners.ts @@ -166,8 +166,8 @@ async function scanClaude(projectRoot: string, home: string, tool: ToolInfo): Pr } } - // Project-level MCP: .claude/.mcp.json or .claude/mcp.json - for (const relPath of [".claude/.mcp.json", ".claude/mcp.json"]) { + // Project-level MCP: .mcp.json (standard), .claude/.mcp.json, or .claude/mcp.json + for (const relPath of [".mcp.json", ".claude/.mcp.json", ".claude/mcp.json"]) { const fullPath = join(projectRoot, relPath); const content = await readTextFile(fullPath); if (content) {