summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/opencode.json8
-rw-r--r--packages/opencode/src/config/config.ts66
-rw-r--r--packages/opencode/src/mcp/index.ts63
-rw-r--r--packages/opencode/src/session/session.ts38
-rw-r--r--packages/opencode/src/util/filesystem.ts4
5 files changed, 149 insertions, 30 deletions
diff --git a/packages/opencode/opencode.json b/packages/opencode/opencode.json
new file mode 100644
index 000000000..1c0c3faa8
--- /dev/null
+++ b/packages/opencode/opencode.json
@@ -0,0 +1,8 @@
+{
+ "mcp": {
+ "planetscale": {
+ "type": "local",
+ "command": ["pscale", "mcp", "server"]
+ }
+ }
+}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 8726b5a3d..4079c971e 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -1,17 +1,53 @@
-import path from "path"
import { Log } from "../util/log"
import { z } from "zod"
import { App } from "../app/app"
import { Provider } from "../provider/provider"
+import { Filesystem } from "../util/filesystem"
export namespace Config {
const log = Log.create({ service: "config" })
export const state = App.state("config", async (app) => {
- const result = await load(app.path.root)
+ let result: Info = {}
+ for (const file of ["opencode.jsonc", "opencode.json"]) {
+ const resolved = await Filesystem.findUp(
+ file,
+ app.path.cwd,
+ app.path.root,
+ )
+ if (!resolved) continue
+ try {
+ result = await import(resolved).then((mod) => Info.parse(mod.default))
+ log.info("found", { path: resolved })
+ break
+ } catch (e) {
+ if (e instanceof z.ZodError) {
+ for (const issue of e.issues) {
+ log.info(issue.message)
+ }
+ throw e
+ }
+ continue
+ }
+ }
+ log.info("loaded", result)
return result
})
+ export const McpLocal = z.object({
+ type: z.literal("local"),
+ command: z.string().array(),
+ environment: z.record(z.string(), z.string()).optional(),
+ })
+
+ export const McpRemote = z.object({
+ type: z.literal("remote"),
+ url: z.string(),
+ })
+
+ export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
+ export type Mcp = z.infer<typeof Mcp>
+
export const Info = z
.object({
provider: z.lazy(() => Provider.Info.array().optional()),
@@ -20,6 +56,7 @@ export namespace Config {
provider: z.record(z.string(), z.string().array()).optional(),
})
.optional(),
+ mcp: z.record(z.string(), Mcp).optional(),
})
.strict()
@@ -28,29 +65,4 @@ export namespace Config {
export function get() {
return state()
}
-
- async function load(directory: string) {
- let result: Info = {}
- for (const file of ["opencode.jsonc", "opencode.json"]) {
- const resolved = path.join(directory, file)
- log.info("searching", { path: resolved })
- try {
- result = await import(path.join(directory, file)).then((mod) =>
- Info.parse(mod.default),
- )
- log.info("found", { path: resolved })
- break
- } catch (e) {
- if (e instanceof z.ZodError) {
- for (const issue of e.issues) {
- log.info(issue.message)
- }
- throw e
- }
- continue
- }
- }
- log.info("loaded", result)
- return result
- }
}
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
new file mode 100644
index 000000000..af52c91a8
--- /dev/null
+++ b/packages/opencode/src/mcp/index.ts
@@ -0,0 +1,63 @@
+import { experimental_createMCPClient, type Tool } from "ai"
+import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
+import { App } from "../app/app"
+import { Config } from "../config/config"
+
+export namespace MCP {
+ const state = App.state(
+ "mcp",
+ 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.type === "remote") {
+ clients[key] = await experimental_createMCPClient({
+ name: key,
+ transport: {
+ type: "sse",
+ url: mcp.url,
+ },
+ })
+ }
+
+ if (mcp.type === "local") {
+ const [cmd, ...args] = mcp.command
+ clients[key] = await experimental_createMCPClient({
+ name: key,
+ transport: new Experimental_StdioMCPTransport({
+ stderr: "ignore",
+ command: cmd,
+ args,
+ env: mcp.environment,
+ }),
+ })
+ }
+ }
+
+ 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())) {
+ result[clientName + "_" + toolName] = tool
+ }
+ }
+ return result
+ }
+}
diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts
index 190c93eea..001af7543 100644
--- a/packages/opencode/src/session/session.ts
+++ b/packages/opencode/src/session/session.ts
@@ -26,6 +26,7 @@ import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { SessionContext } from "./context"
import { ListTool } from "../tool/ls"
+import { MCP } from "../mcp"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -342,6 +343,38 @@ ${app.git ? await ListTool.execute({ path: app.path.cwd }, { sessionID: input.se
},
})
}
+ for (const [key, item] of Object.entries(await MCP.tools())) {
+ const execute = item.execute
+ if (!execute) continue
+ item.execute = async (args, opts) => {
+ const start = Date.now()
+ try {
+ const result = await execute(args, opts)
+ next.metadata!.tool![opts.toolCallId] = {
+ ...result.metadata,
+ time: {
+ start,
+ end: Date.now(),
+ },
+ }
+ return result.content
+ .filter((x: any) => x.type === "text")
+ .map((x: any) => x.text)
+ .join("\n\n")
+ } catch (e: any) {
+ next.metadata!.tool![opts.toolCallId] = {
+ error: true,
+ message: e.toString(),
+ time: {
+ start,
+ end: Date.now(),
+ },
+ }
+ return e.toString()
+ }
+ }
+ tools[key] = item
+ }
const result = streamText({
onStepFinish: async (step) => {
const assistant = next.metadata!.assistant!
@@ -356,7 +389,10 @@ ${app.git ? await ListTool.execute({ path: app.path.cwd }, { sessionID: input.se
stopWhen: stepCountIs(1000),
messages: convertToModelMessages(msgs),
temperature: 0,
- tools,
+ tools: {
+ ...(await MCP.tools()),
+ ...tools,
+ },
model: model.language,
})
let text: Message.TextPart | undefined
diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts
index b85fbd72a..ac85ca35e 100644
--- a/packages/opencode/src/util/filesystem.ts
+++ b/packages/opencode/src/util/filesystem.ts
@@ -2,11 +2,12 @@ import { exists } from "fs/promises"
import { dirname, join } from "path"
export namespace Filesystem {
- export async function findUp(target: string, start: string) {
+ export async function findUp(target: string, start: string, stop?: string) {
let currentDir = start
while (true) {
const targetPath = join(currentDir, target)
if (await exists(targetPath)) return targetPath
+ if (stop === currentDir) return
const parentDir = dirname(currentDir)
if (parentDir === currentDir) {
return
@@ -15,4 +16,3 @@ export namespace Filesystem {
}
}
}
-