diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 0c0b8b54f..b25b66888 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -509,7 +509,18 @@ export namespace ACP { await Promise.all( Object.entries(mcpServers).map(async ([key, mcp]) => { - await MCP.add(key, mcp) + await this.sdk.mcp + .add({ + throwOnError: true, + query: { directory }, + body: { + name: key, + config: mcp, + }, + }) + .catch((error) => { + log.error("failed to add mcp server", { name: key, error }) + }) }), ) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index fbc887030..eddd19249 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -92,13 +92,28 @@ export namespace MCP { export async function add(name: string, mcp: Config.Mcp) { const s = await state() const result = await create(name, mcp) - if (!result) return + if (!result) { + const status = { + status: "failed" as const, + error: "unknown error", + } + s.status[name] = status + return { + status, + } + } if (!result.mcpClient) { s.status[name] = result.status - return + return { + status: s.status, + } } s.clients[name] = result.mcpClient s.status[name] = result.status + + return { + status: s.status, + } } async function create(key: string, mcp: Config.Mcp) { @@ -207,8 +222,12 @@ export namespace MCP { } } - const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch(() => {}) + const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch((err) => { + log.error("create() failed to get tools from client", { key, error: err }) + return undefined + }) if (!result) { + log.info("create() tools() returned nothing, closing client", { key }) await mcpClient.close().catch((error) => { log.error("Failed to close MCP client", { error, @@ -227,6 +246,7 @@ export namespace MCP { } } + log.info("create() successfully created client", { key, toolCount: Object.keys(result).length }) return { mcpClient, status, @@ -238,13 +258,18 @@ export namespace MCP { } export async function clients() { - return state().then((state) => state.clients) + const s = await state() + log.info("clients() called", { clientCount: Object.keys(s.clients).length }) + return s.clients } export async function tools() { const result: Record = {} const s = await state() - for (const [clientName, client] of Object.entries(await clients())) { + log.info("tools() called", { clientCount: Object.keys(s.clients).length }) + const clientsSnapshot = await clients() + for (const [clientName, client] of Object.entries(clientsSnapshot)) { + log.info("tools() fetching tools for client", { clientName }) const tools = await client.tools().catch((e) => { log.error("failed to get tools", { clientName, error: e.message }) const failedStatus = { @@ -255,14 +280,17 @@ export namespace MCP { delete s.clients[clientName] }) if (!tools) { + log.info("tools() no tools returned for client", { clientName }) continue } + log.info("tools() got tools for client", { clientName, toolCount: Object.keys(tools).length }) for (const [toolName, tool] of Object.entries(tools)) { const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_") const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_") result[sanitizedClientName + "_" + sanitizedToolName] = tool } } + log.info("tools() final result", { toolCount: Object.keys(result).length }) return result } } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index bb9f065e0..7cf091cc0 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1359,6 +1359,36 @@ export namespace Server { return c.json(await MCP.status()) }, ) + .post( + "/mcp", + describeRoute({ + description: "Add MCP server dynamically", + operationId: "mcp.add", + responses: { + 200: { + description: "MCP server added successfully", + content: { + "application/json": { + schema: resolver(z.record(z.string(), MCP.Status)), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + name: z.string(), + config: Config.Mcp, + }), + ), + async (c) => { + const { name, config } = c.req.valid("json") + const result = await MCP.add(name, config) + return c.json(result.status) + }, + ) .get( "/lsp", describeRoute({ diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 1a54da8fa..f902d91ab 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -106,6 +106,9 @@ import type { AppAgentsResponses, McpStatusData, McpStatusResponses, + McpAddData, + McpAddResponses, + McpAddErrors, LspStatusData, LspStatusResponses, FormatterStatusData, @@ -764,6 +767,20 @@ class Mcp extends _HeyApiClient { ...options, }) } + + /** + * Add MCP server dynamically + */ + public add(options?: Options) { + return (options?.client ?? this._client).post({ + url: "/mcp", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }) + } } class Lsp extends _HeyApiClient { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 5f565df58..2a0df3024 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1979,9 +1979,9 @@ export type SessionMessagesData = { */ id: string } - query?: { + query: { directory?: string - limit?: number + limit: number } url: "/session/{id}/message" } @@ -2552,6 +2552,38 @@ export type McpStatusResponses = { export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] +export type McpAddData = { + body?: { + name: string + config: McpLocalConfig | McpRemoteConfig + } + path?: never + query?: { + directory?: string + } + url: "/mcp" +} + +export type McpAddErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpAddError = McpAddErrors[keyof McpAddErrors] + +export type McpAddResponses = { + /** + * MCP server added successfully + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpAddResponse = McpAddResponses[keyof McpAddResponses] + export type LspStatusData = { body?: never path?: never