2026-02-08 16:21:28 +09:00
|
|
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
|
|
|
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
|
|
import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types"
|
|
|
|
|
import { createCleanMcpEnvironment } from "./env-cleaner"
|
|
|
|
|
import { registerProcessCleanup, startCleanupTimer } from "./cleanup"
|
2026-04-08 13:10:06 +09:00
|
|
|
import { redactSensitiveData } from "./error-redaction"
|
2026-04-10 15:53:02 +09:00
|
|
|
import type { ManagedClient, McpClient, McpTransport, SkillMcpClientConnectionParams } from "./types"
|
|
|
|
|
|
|
|
|
|
type StdioClientFactory = (
|
|
|
|
|
clientInfo: { name: string; version: string },
|
|
|
|
|
options: { capabilities: Record<string, never> }
|
|
|
|
|
) => McpClient
|
|
|
|
|
|
|
|
|
|
type StdioTransportFactory = (
|
|
|
|
|
options: ConstructorParameters<typeof StdioClientTransport>[0]
|
|
|
|
|
) => McpTransport
|
|
|
|
|
|
|
|
|
|
interface StdioClientDependencies {
|
|
|
|
|
createClient: StdioClientFactory
|
|
|
|
|
createTransport: StdioTransportFactory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultStdioClientDependencies: StdioClientDependencies = {
|
|
|
|
|
createClient: (clientInfo, options) => new Client(clientInfo, options),
|
|
|
|
|
createTransport: (options) => new StdioClientTransport(options),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stdioClientDependencies: StdioClientDependencies = defaultStdioClientDependencies
|
|
|
|
|
|
|
|
|
|
export function setStdioClientDependenciesForTesting(
|
|
|
|
|
dependencies?: Partial<StdioClientDependencies>
|
|
|
|
|
): void {
|
|
|
|
|
stdioClientDependencies = dependencies
|
|
|
|
|
? {
|
|
|
|
|
...defaultStdioClientDependencies,
|
|
|
|
|
...dependencies,
|
|
|
|
|
}
|
|
|
|
|
: defaultStdioClientDependencies
|
|
|
|
|
}
|
2026-02-08 16:21:28 +09:00
|
|
|
|
|
|
|
|
function getStdioCommand(config: ClaudeCodeMcpServer, serverName: string): string {
|
|
|
|
|
if (!config.command) {
|
|
|
|
|
throw new Error(`MCP server "${serverName}" is configured for stdio but missing 'command' field.`)
|
|
|
|
|
}
|
|
|
|
|
return config.command
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 15:53:02 +09:00
|
|
|
export async function createStdioClient(params: SkillMcpClientConnectionParams): Promise<McpClient> {
|
2026-02-08 16:21:28 +09:00
|
|
|
const { state, clientKey, info, config } = params
|
2026-03-11 21:16:37 +09:00
|
|
|
const shutdownGenAtStart = state.shutdownGeneration
|
2026-02-08 16:21:28 +09:00
|
|
|
|
|
|
|
|
const command = getStdioCommand(config, info.serverName)
|
|
|
|
|
const args = config.args ?? []
|
|
|
|
|
const mergedEnv = createCleanMcpEnvironment(config.env)
|
|
|
|
|
|
|
|
|
|
registerProcessCleanup(state)
|
|
|
|
|
|
2026-04-10 15:53:02 +09:00
|
|
|
const transport: McpTransport = stdioClientDependencies.createTransport({
|
2026-02-08 16:21:28 +09:00
|
|
|
command,
|
|
|
|
|
args,
|
|
|
|
|
env: mergedEnv,
|
|
|
|
|
stderr: "ignore",
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-10 15:53:02 +09:00
|
|
|
const client: McpClient = stdioClientDependencies.createClient(
|
2026-02-08 16:21:28 +09:00
|
|
|
{ name: `skill-mcp-${info.skillName}-${info.serverName}`, version: "1.0.0" },
|
|
|
|
|
{ capabilities: {} }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await client.connect(transport)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Close transport to prevent orphaned MCP process on connection failure
|
|
|
|
|
try {
|
|
|
|
|
await transport.close()
|
|
|
|
|
} catch {
|
|
|
|
|
// Process may already be terminated
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
2026-04-08 13:10:06 +09:00
|
|
|
const fullCommand = `${command} ${args.join(" ")}`
|
|
|
|
|
const safeCommand = redactSensitiveData(fullCommand)
|
|
|
|
|
const safeErrorMessage = redactSensitiveData(errorMessage)
|
2026-02-08 16:21:28 +09:00
|
|
|
throw new Error(
|
|
|
|
|
`Failed to connect to MCP server "${info.serverName}".\n\n` +
|
2026-04-08 13:10:06 +09:00
|
|
|
`Command: ${safeCommand}\n` +
|
|
|
|
|
`Reason: ${safeErrorMessage}\n\n` +
|
2026-02-08 16:21:28 +09:00
|
|
|
`Hints:\n` +
|
|
|
|
|
` - Ensure the command is installed and available in PATH\n` +
|
|
|
|
|
` - Check if the MCP server package exists\n` +
|
|
|
|
|
` - Verify the args are correct for this server`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 21:16:37 +09:00
|
|
|
if (state.shutdownGeneration !== shutdownGenAtStart) {
|
|
|
|
|
try { await client.close() } catch {}
|
|
|
|
|
try { await transport.close() } catch {}
|
|
|
|
|
throw new Error(`MCP server "${info.serverName}" connection completed after shutdown`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 16:21:28 +09:00
|
|
|
const managedClient = {
|
|
|
|
|
client,
|
|
|
|
|
transport,
|
|
|
|
|
skillName: info.skillName,
|
|
|
|
|
lastUsedAt: Date.now(),
|
|
|
|
|
connectionType: "stdio",
|
|
|
|
|
} satisfies ManagedClient
|
|
|
|
|
|
|
|
|
|
state.clients.set(clientKey, managedClient)
|
|
|
|
|
startCleanupTimer(state)
|
|
|
|
|
return client
|
|
|
|
|
}
|