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:
Aiden Cline
2025-10-28 00:08:30 -05:00
committed by GitHub
parent 4caa458232
commit 982954cc1b
3 changed files with 319 additions and 170 deletions

View File

@@ -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"> = {}