mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-04 08:03:14 +00:00
feat (acp): mcp server support, file diffs, some default slash commands (/init, /compact), show todos properly (#3490)
The mcp server support does not mean acp didn't allow u to use mcp servers previously, it means that now you can connect new servers via ACP instead of relying on the opencode defined ones
This commit is contained in:
@@ -26,122 +26,22 @@ export namespace MCP {
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const cfg = await Config.get()
|
||||
const config = cfg.mcp ?? {}
|
||||
const clients: {
|
||||
[name: string]: MCPClient
|
||||
} = {}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
delete clients[key]
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Object.entries(config).map(async ([key, mcp]) => {
|
||||
const result = await create(key, mcp).catch(() => undefined)
|
||||
if (!result) return
|
||||
clients[key] = result.client
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
clients,
|
||||
config: cfg.mcp ?? {},
|
||||
config,
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
@@ -151,6 +51,133 @@ 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
|
||||
s.clients[name] = result.client
|
||||
}
|
||||
|
||||
async function create(name: string, mcp: Config.Mcp) {
|
||||
if (mcp.enabled === false) {
|
||||
log.info("mcp server disabled", { name })
|
||||
return
|
||||
}
|
||||
log.info("found", { name, type: mcp.type })
|
||||
|
||||
let mcpClient: MCPClient | undefined
|
||||
|
||||
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", {
|
||||
name,
|
||||
transport: name,
|
||||
url: mcp.url,
|
||||
error: lastError.message,
|
||||
})
|
||||
return null
|
||||
})
|
||||
if (client) {
|
||||
log.debug("transport connection succeeded", { name, transport: name })
|
||||
mcpClient = client
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!mcpClient) {
|
||||
const errorMessage = lastError
|
||||
? `MCP server ${name} failed to connect: ${lastError.message}`
|
||||
: `MCP server ${name} failed to connect to ${mcp.url}`
|
||||
log.error("remote mcp connection failed", { name, 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 ${name} failed to start: ${error.message}`
|
||||
: `MCP server ${name} failed to start`
|
||||
log.error("local mcp startup failed", {
|
||||
name,
|
||||
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) {
|
||||
mcpClient = client
|
||||
}
|
||||
}
|
||||
|
||||
if (!mcpClient) {
|
||||
log.warn("mcp client not initialized", { name })
|
||||
return
|
||||
}
|
||||
|
||||
const result = await withTimeout(mcpClient.tools(), 5000).catch(() => {})
|
||||
if (!result) {
|
||||
log.warn("mcp client verification failed, dropping client", { name })
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
client: mcpClient,
|
||||
}
|
||||
}
|
||||
|
||||
export async function status() {
|
||||
return state().then((state) => {
|
||||
const result: Record<string, "connected" | "failed" | "disabled"> = {}
|
||||
|
||||
Reference in New Issue
Block a user