Skip to content

Commit 6945498

Browse files
committed
Merge branch 'dev' into feat/keybindable-commands
2 parents 1a8503d + 9188bc5 commit 6945498

File tree

6 files changed

+81
-44
lines changed

6 files changed

+81
-44
lines changed

.github/workflows/stats.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ on:
55
- cron: "0 12 * * *" # Run daily at 12:00 UTC
66
workflow_dispatch: # Allow manual trigger
77

8+
concurrency: ${{ github.workflow }}-${{ github.ref }}
9+
810
jobs:
911
stats:
12+
if: github.repository == 'sst/opencode'
1013
runs-on: blacksmith-4vcpu-ubuntu-2404
1114
permissions:
1215
contents: write

bun.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"name": "opencode",
77
"dependencies": {
88
"@aws-sdk/client-s3": "3.933.0",
9+
"@opencode-ai/plugin": "workspace:*",
910
"@opencode-ai/script": "workspace:*",
1011
"@opencode-ai/sdk": "workspace:*",
1112
"typescript": "catalog:",

nix/hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"nodeModules": "sha256-cpXmqJQJeFj3eED/aOb4YLUdkZFV//7u4f0STBxzUhk="
2+
"nodeModules": "sha256-XhU8gEwLPUtzFhMfg+QxExn5/WiDo5VVOiZ0AmklRwc="
33
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"@aws-sdk/client-s3": "3.933.0",
6767
"@opencode-ai/script": "workspace:*",
6868
"@opencode-ai/sdk": "workspace:*",
69+
"@opencode-ai/plugin": "workspace:*",
6970
"typescript": "catalog:"
7071
},
7172
"repository": {

packages/opencode/src/lsp/language.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
2525
".ex": "elixir",
2626
".exs": "elixir",
2727
".erl": "erlang",
28+
".ets": "typescript",
2829
".hrl": "erlang",
2930
".fs": "fsharp",
3031
".fsi": "fsharp",

packages/opencode/src/mcp/index.ts

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { type Tool } from "ai"
2-
import { experimental_createMCPClient } from "@ai-sdk/mcp"
1+
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
2+
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
33
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
44
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
55
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
66
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
7+
import type { Tool as MCPToolDef } from "@modelcontextprotocol/sdk/types.js"
78
import { Config } from "../config/config"
89
import { Log } from "../util/log"
910
import { NamedError } from "@opencode-ai/util/error"
1011
import z from "zod/v4"
1112
import { Instance } from "../project/instance"
13+
import { Installation } from "../installation"
1214
import { withTimeout } from "@/util/timeout"
1315
import { McpOAuthProvider } from "./oauth-provider"
1416
import { McpOAuthCallback } from "./oauth-callback"
@@ -25,7 +27,7 @@ export namespace MCP {
2527
}),
2628
)
2729

28-
type Client = Awaited<ReturnType<typeof experimental_createMCPClient>>
30+
type MCPClient = Client
2931

3032
export const Status = z
3133
.discriminatedUnion("status", [
@@ -71,7 +73,30 @@ export namespace MCP {
7173
ref: "MCPStatus",
7274
})
7375
export type Status = z.infer<typeof Status>
74-
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>
76+
77+
// Convert MCP tool definition to AI SDK Tool type
78+
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient): Tool {
79+
const inputSchema = mcpTool.inputSchema
80+
81+
// Spread first, then override type to ensure it's always "object"
82+
const schema: JSONSchema7 = {
83+
...(inputSchema as JSONSchema7),
84+
type: "object",
85+
properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
86+
additionalProperties: false,
87+
}
88+
89+
return dynamicTool({
90+
description: mcpTool.description ?? "",
91+
inputSchema: jsonSchema(schema),
92+
execute: async (args: unknown) => {
93+
return client.callTool({
94+
name: mcpTool.name,
95+
arguments: args as Record<string, unknown>,
96+
})
97+
},
98+
})
99+
}
75100

76101
// Store transports for OAuth servers to allow finishing auth
77102
type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
@@ -81,7 +106,7 @@ export namespace MCP {
81106
async () => {
82107
const cfg = await Config.get()
83108
const config = cfg.mcp ?? {}
84-
const clients: Record<string, Client> = {}
109+
const clients: Record<string, MCPClient> = {}
85110
const status: Record<string, Status> = {}
86111

87112
await Promise.all(
@@ -204,10 +229,12 @@ export namespace MCP {
204229
let lastError: Error | undefined
205230
for (const { name, transport } of transports) {
206231
try {
207-
mcpClient = await experimental_createMCPClient({
232+
const client = new Client({
208233
name: "opencode",
209-
transport,
234+
version: Installation.VERSION,
210235
})
236+
await client.connect(transport)
237+
mcpClient = client
211238
log.info("connected", { key, transport: name })
212239
status = { status: "connected" }
213240
break
@@ -248,36 +275,38 @@ export namespace MCP {
248275

249276
if (mcp.type === "local") {
250277
const [cmd, ...args] = mcp.command
251-
await experimental_createMCPClient({
252-
name: "opencode",
253-
transport: new StdioClientTransport({
254-
stderr: "ignore",
255-
command: cmd,
256-
args,
257-
env: {
258-
...process.env,
259-
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
260-
...mcp.environment,
261-
},
262-
}),
278+
const transport = new StdioClientTransport({
279+
stderr: "ignore",
280+
command: cmd,
281+
args,
282+
env: {
283+
...process.env,
284+
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
285+
...mcp.environment,
286+
},
263287
})
264-
.then((client) => {
265-
mcpClient = client
266-
status = {
267-
status: "connected",
268-
}
288+
289+
try {
290+
const client = new Client({
291+
name: "opencode",
292+
version: Installation.VERSION,
269293
})
270-
.catch((error) => {
271-
log.error("local mcp startup failed", {
272-
key,
273-
command: mcp.command,
274-
error: error instanceof Error ? error.message : String(error),
275-
})
276-
status = {
277-
status: "failed" as const,
278-
error: error instanceof Error ? error.message : String(error),
279-
}
294+
await client.connect(transport)
295+
mcpClient = client
296+
status = {
297+
status: "connected",
298+
}
299+
} catch (error) {
300+
log.error("local mcp startup failed", {
301+
key,
302+
command: mcp.command,
303+
error: error instanceof Error ? error.message : String(error),
280304
})
305+
status = {
306+
status: "failed" as const,
307+
error: error instanceof Error ? error.message : String(error),
308+
}
309+
}
281310
}
282311

283312
if (!status) {
@@ -294,7 +323,7 @@ export namespace MCP {
294323
}
295324
}
296325

297-
const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch((err) => {
326+
const result = await withTimeout(mcpClient.listTools(), mcp.timeout ?? 5000).catch((err) => {
298327
log.error("failed to get tools from client", { key, error: err })
299328
return undefined
300329
})
@@ -317,7 +346,7 @@ export namespace MCP {
317346
}
318347
}
319348

320-
log.info("create() successfully created client", { key, toolCount: Object.keys(result).length })
349+
log.info("create() successfully created client", { key, toolCount: result.tools.length })
321350
return {
322351
mcpClient,
323352
status,
@@ -392,22 +421,23 @@ export namespace MCP {
392421
continue
393422
}
394423

395-
const tools = await client.tools().catch((e) => {
424+
const toolsResult = await client.listTools().catch((e) => {
396425
log.error("failed to get tools", { clientName, error: e.message })
397426
const failedStatus = {
398427
status: "failed" as const,
399428
error: e instanceof Error ? e.message : String(e),
400429
}
401430
s.status[clientName] = failedStatus
402431
delete s.clients[clientName]
432+
return undefined
403433
})
404-
if (!tools) {
434+
if (!toolsResult) {
405435
continue
406436
}
407-
for (const [toolName, tool] of Object.entries(tools)) {
437+
for (const mcpTool of toolsResult.tools) {
408438
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
409-
const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_")
410-
result[sanitizedClientName + "_" + sanitizedToolName] = tool
439+
const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
440+
result[sanitizedClientName + "_" + sanitizedToolName] = convertMcpTool(mcpTool, client)
411441
}
412442
}
413443
return result
@@ -469,10 +499,11 @@ export namespace MCP {
469499

470500
// Try to connect - this will trigger the OAuth flow
471501
try {
472-
await experimental_createMCPClient({
502+
const client = new Client({
473503
name: "opencode",
474-
transport,
504+
version: Installation.VERSION,
475505
})
506+
await client.connect(transport)
476507
// If we get here, we're already authenticated
477508
return { authorizationUrl: "" }
478509
} catch (error) {

0 commit comments

Comments
 (0)