| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- import { experimental_createMCPClient, type Tool } from "ai"
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
- 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"
- export namespace MCP {
- const log = Log.create({ service: "mcp" })
- export const Failed = NamedError.create(
- "MCPFailed",
- z.object({
- name: z.string(),
- }),
- )
- const state = Instance.state(
- async () => {
- const cfg = await Config.get()
- const clients: {
- [name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
- } = {}
- for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
- if (mcp.enabled === false) {
- log.info("mcp server disabled", { key })
- continue
- }
- log.info("found", { key, type: mcp.type })
- if (mcp.type === "remote") {
- const transports = [
- {
- name: "StreamableHTTP",
- transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
- requestInit: {
- headers: mcp.headers,
- },
- }),
- },
- {
- name: "SSE",
- transport: new SSEClientTransport(new URL(mcp.url), {
- requestInit: {
- headers: mcp.headers,
- },
- }),
- },
- ]
- let lastError: Error | undefined
- for (const { name, transport } of transports) {
- const client = 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,
- })
- return null
- })
- if (client) {
- log.debug("transport connection succeeded", { key, transport: name })
- 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({
- name: "opencode",
- transport: new StdioClientTransport({
- stderr: "ignore",
- command: cmd,
- args,
- env: {
- ...process.env,
- ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
- ...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
- }
- }
- }
- return {
- clients,
- }
- },
- async (state) => {
- for (const client of Object.values(state.clients)) {
- client.close()
- }
- },
- )
- export async function clients() {
- return state().then((state) => state.clients)
- }
- export async function tools() {
- const result: Record<string, Tool> = {}
- for (const [clientName, client] of Object.entries(await clients())) {
- for (const [toolName, tool] of Object.entries(await client.tools())) {
- const sanitizedClientName = clientName.replace(/\s+/g, "_")
- const sanitizedToolName = toolName.replace(/[-\s]+/g, "_")
- result[sanitizedClientName + "_" + sanitizedToolName] = tool
- }
- }
- return result
- }
- }
|