fix: discover MCP servers from project-root .mcp.json (#692)
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
This commit is contained in:
parent
a90aa0c8d6
commit
7b11faa150
2 changed files with 75 additions and 4 deletions
|
|
@ -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<string>): 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<string, unknown>;
|
||||
const mcpServers = (data.mcpServers ?? data.servers) as Record<string, Record<string, unknown>> | 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<McpServer[]> {
|
||||
if (serverListCache) return serverListCache;
|
||||
|
||||
|
|
@ -112,6 +145,14 @@ async function getServerList(signal?: AbortSignal): Promise<McpServer[]> {
|
|||
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<McpServerDetail> {
|
||||
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<string, unknown>;
|
||||
const mcpServers = (data.mcpServers ?? data.servers) as Record<string, Record<string, unknown>> | 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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue