diff --git a/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json b/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json new file mode 100644 index 000000000..7c57ef350 --- /dev/null +++ b/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 14 + }, + "auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json", + "files": [ + { + "date": 1759827172859, + "name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log", + "hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/mcp-puppeteer-2025-10-07.log b/logs/mcp-puppeteer-2025-10-07.log new file mode 100644 index 000000000..218cea564 --- /dev/null +++ b/logs/mcp-puppeteer-2025-10-07.log @@ -0,0 +1,38 @@ +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.879"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:52.880"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.191"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:56.192"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.267"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:52:59.268"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.276"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:20.277"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.838"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:30.839"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:42.452"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.499"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:53:46.500"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:02.295"} +{"arguments":{"url":"https://google.com"},"level":"debug","message":"Tool call received","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150","tool":"puppeteer_navigate"} +{"0":"n","1":"p","2":"x","level":"info","message":"Launching browser with config:","service":"mcp-puppeteer","timestamp":"2025-10-07 04:54:37.150"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.488"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 04:55:08.489"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.815"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:11.816"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.934"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:21.935"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:32.544"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.154"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:41.155"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.426"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:23:55.427"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.715"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:15.716"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.063"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:25.064"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.567"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:24:48.568"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.937"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-10-07 05:25:08.938"} diff --git a/opencode.json b/opencode.json index 720ece5c1..4b774c647 100644 --- a/opencode.json +++ b/opencode.json @@ -1,3 +1,13 @@ { - "$schema": "https://opencode.ai/config.json" + "$schema": "https://opencode.ai/config.json", + "mcp": { + "weather": { + "type": "local", + "command": ["bun", "x", "@h1deya/mcp-server-weather"] + }, + "puppeteer": { + "type": "local", + "command": ["buns", "x", "puppeteer-mcp-server"] + } + } } diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index cad76db59..e7a760c5a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,36 +1,77 @@ import { Installation } from "@/installation" import { TextAttributes } from "@opentui/core" import { Prompt } from "@tui/component/prompt" -import { For } from "solid-js" +import { createResource, For, Match, Show, Suspense, Switch, type ParentProps } from "solid-js" import { Theme } from "@tui/context/theme" +import { useSDK } from "../context/sdk" +import { useKeybind } from "../context/keybind" +import type { KeybindsConfig } from "@opencode-ai/sdk" export function Home() { + const sdk = useSDK() + const [mcp] = createResource(async () => { + const result = await sdk.mcp.status() + return result.data + }) return ( - - - - - new session - show help - share session - list models - list agents - + + + + Commands + List sessions + Switch model + Switch agent - + + 10}> + + + {([key, item]) => ( + + + • + + + {key} (MCP){" "} + + + + <> + + {(val) => val().error} + Disabled in configuration + + + + + )} + + + + + ) } -function HelpRow(props: { children: string; slash: string }) { +function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) { + const keybind = useKeybind() return ( - - /{props.slash.padEnd(10, " ")} - {props.children.padEnd(19, " ")} - ctrl+x n - + + {props.children} + {keybind.print(props.keybind)} + ) } diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 0ced9271f..bf4a490c5 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -6,8 +6,6 @@ import { Config } from "../config/config" import { Log } from "../util/log" import { NamedError } from "../util/error" import z from "zod/v4" -import { Session } from "../session" -import { Bus } from "../bus" import { Instance } from "../project/instance" import { withTimeout } from "@/util/timeout" @@ -21,14 +19,43 @@ export namespace MCP { }), ) - type MCPClient = Awaited> + type Client = Awaited> + + export const Status = z + .discriminatedUnion("status", [ + z + .object({ + status: z.literal("connected"), + }) + .meta({ + ref: "MCPStatusConnected", + }), + z + .object({ + status: z.literal("disabled"), + }) + .meta({ + ref: "MCPStatusDisabled", + }), + z + .object({ + status: z.literal("failed"), + error: z.string(), + }) + .meta({ + ref: "MCPStatusFailed", + }), + ]) + .meta({ + ref: "MCPStatus", + }) + export type Status = z.infer const state = Instance.state( async () => { const cfg = await Config.get() - const clients: { - [name: string]: MCPClient - } = {} + const clients: Record = {} + const status: Record = {} for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) { if (mcp.enabled === false) { log.info("mcp server disabled", { key }) @@ -56,51 +83,35 @@ export namespace MCP { ] let lastError: Error | undefined for (const { name, transport } of transports) { - const client = await experimental_createMCPClient({ + await experimental_createMCPClient({ name: "opencode", transport, - }).catch((error) => { - lastError = error instanceof Error ? error : new Error(String(error)) - log.debug("transport connection failed", { - key, - transport: name, - url: mcp.url, - error: lastError.message, + }) + .then((client) => { + clients[key] = client + status[key] = { + status: "connected", + } }) - return null - }) - if (client) { - log.debug("transport connection succeeded", { - key, - transport: name, + .catch((error) => { + lastError = error instanceof Error ? error : new Error(String(error)) + log.debug("transport connection failed", { + key, + transport: name, + url: mcp.url, + error: lastError.message, + }) + status[key] = { + status: "failed", + error: lastError.message, + } }) - clients[key] = client - break - } - } - if (!clients[key]) { - const errorMessage = lastError - ? `MCP server ${key} failed to connect: ${lastError.message}` - : `MCP server ${key} failed to connect to ${mcp.url}` - log.error("remote mcp connection failed", { - key, - url: mcp.url, - error: lastError?.message, - }) - Bus.publish(Session.Event.Error, { - error: { - name: "UnknownError", - data: { - message: errorMessage, - }, - }, - }) } } if (mcp.type === "local") { const [cmd, ...args] = mcp.command - const client = await experimental_createMCPClient({ + await experimental_createMCPClient({ name: "opencode", transport: new StdioClientTransport({ stderr: "ignore", @@ -112,43 +123,42 @@ export namespace MCP { ...mcp.environment, }, }), - }).catch((error) => { - const errorMessage = - error instanceof Error - ? `MCP server ${key} failed to start: ${error.message}` - : `MCP server ${key} failed to start` - log.error("local mcp startup failed", { - key, - command: mcp.command, - error: error instanceof Error ? error.message : String(error), - }) - Bus.publish(Session.Event.Error, { - error: { - name: "UnknownError", - data: { - message: errorMessage, - }, - }, - }) - return null }) - if (client) { - clients[key] = client - } + .then((client) => { + clients[key] = client + status[key] = { + status: "connected", + } + }) + .catch((error) => { + log.error("local mcp startup failed", { + key, + command: mcp.command, + error: error instanceof Error ? error.message : String(error), + }) + status[key] = { + status: "failed", + error: error instanceof Error ? error.message : String(error), + } + }) } } for (const [key, client] of Object.entries(clients)) { const result = await withTimeout(client.tools(), 5000).catch(() => {}) if (!result) { - log.warn("mcp client verification failed, removing client", { key }) + client.close() delete clients[key] + status[key] = { + status: "failed", + error: "Failed to get tools", + } } } return { + status, clients, - config: cfg.mcp ?? {}, } }, async (state) => { @@ -159,20 +169,7 @@ export namespace MCP { ) export async function status() { - return state().then((state) => { - const result: Record = {} - for (const [key, client] of Object.entries(state.config)) { - if (client.enabled === false) { - result[key] = "disabled" - continue - } - if (state.clients[key]) { - result[key] = "connected" - } - result[key] = "failed" - } - return result - }) + return state().then((state) => state.status) } export async function clients() { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index ee04b1f92..2ec7f25c8 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1194,7 +1194,7 @@ export namespace Server { description: "MCP server status", content: { "application/json": { - schema: resolver(z.any()), + schema: resolver(z.record(z.string(), MCP.Status)), }, }, }, diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts index eb6518326..d6ff7b26a 100644 --- a/packages/opencode/src/util/keybind.ts +++ b/packages/opencode/src/util/keybind.ts @@ -27,7 +27,7 @@ export namespace Keybind { let result = parts.join("+") if (info.leader) { - result = result ? `,${result}` : `` + result = result ? ` ${result}` : `` } return result diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 2bfc671a9..093503eb6 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1005,6 +1005,21 @@ export type Agent = { } } +export type McpStatusConnected = { + status: "connected" +} + +export type McpStatusDisabled = { + status: "disabled" +} + +export type McpStatusFailed = { + status: "failed" + error: string +} + +export type McpStatus = McpStatusConnected | McpStatusDisabled | McpStatusFailed + export type OAuth = { type: "oauth" refresh: string @@ -2086,9 +2101,13 @@ export type McpStatusResponses = { /** * MCP server status */ - 200: unknown + 200: { + [key: string]: McpStatus + } } +export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] + export type TuiAppendPromptData = { body?: { text: string