Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions packages/core/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,7 @@ export const layer = (options?: Options) =>
})
.pipe(Scope.provide(scope))
})
const requireServer = Effect.fnUntraced(function* (server: ServerName | string) {
const name = ServerName.make(server)
const requireServer = Effect.fnUntraced(function* (name: ServerName) {
const entry = entries.get(name)
if (!entry) return yield* new NotFoundError({ server: name })
return { name, entry }
Expand Down Expand Up @@ -667,7 +666,7 @@ export const layer = (options?: Options) =>
.toSorted((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name))
}),
callTool: Effect.fn("MCP.callTool")(function* (input) {
const target = yield* requireServer(input.server)
const target = yield* requireServer(ServerName.make(input.server))
yield* target.entry.startup.await
if (!target.entry.client)
return yield* new ToolCallError({
Expand Down Expand Up @@ -704,7 +703,7 @@ export const layer = (options?: Options) =>
.toSorted((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name))
}),
prompt: Effect.fn("MCP.prompt")(function* (input) {
const target = yield* requireServer(input.server)
const target = yield* requireServer(ServerName.make(input.server))
yield* target.entry.startup.await
if (!target.entry.client) return undefined
const result = yield* recovering(target.name, target.entry, target.entry.client, (connection) =>
Expand All @@ -726,13 +725,13 @@ export const layer = (options?: Options) =>
return mergeCatalogs(catalogs)
}),
resources: Effect.fn("MCP.resources")(function* (input) {
const target = yield* requireServer(input.server)
const target = yield* requireServer(ServerName.make(input.server))
yield* target.entry.startup.await
if (!target.entry.client) return ResourceCatalog.make({ resources: [], templates: [] })
return mergeCatalogs([yield* loadCatalog(target.name, target.entry, target.entry.client)])
}),
readResource: Effect.fn("MCP.readResource")(function* (input) {
const target = yield* requireServer(input.server)
const target = yield* requireServer(ServerName.make(input.server))
yield* target.entry.startup.await
if (!target.entry.client) return undefined
const result = yield* recovering(target.name, target.entry, target.entry.client, (connection) =>
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/tool/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * as McpTool from "./mcp.js"

import { ToolFailure } from "@opencode/ai"
import { McpEvent } from "@opencode/schema/mcp-event"
import { Context, Effect, Fiber, type JsonSchema, Layer, PubSub, Semaphore, Stream } from "effect"
import { Context, Effect, Fiber, type JsonSchema, Layer, Option, PubSub, Schema, Semaphore, Stream } from "effect"
import { makeLocationNode } from "@opencode/util/effect/app-node"
import { extension } from "mime-types"
import { Bus } from "../bus.js"
Expand All @@ -14,6 +14,8 @@ import { Tool } from "../tool.js"
import { ToolOutput } from "../tool-output.js"
import { ReadToolFileSystem } from "./read-filesystem.js"

const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))

/**
* Registry namespace and permission action names for MCP tools.
*/
Expand Down Expand Up @@ -94,11 +96,8 @@ export const layer = Layer.effect(
if (result.structured !== undefined) return result.structured
if (text === "") return null
// Agents assume JSON returned as text is already an object, so parse it when the server declares no schema.
if (tool.outputSchema === undefined && (text.startsWith("{") || text.startsWith("["))) {
try {
return JSON.parse(text)
} catch {}
}
if (tool.outputSchema === undefined && (text.startsWith("{") || text.startsWith("[")))
return Option.getOrElse(decodeJson(text), () => text)
return text
}
return {
Expand Down
6 changes: 0 additions & 6 deletions packages/core/src/tool/plugin/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ export const Plugin = {
Effect.catch(() => Effect.void),
Effect.catchDefect(() => Effect.void),
)
if (
result.content.type === "file" &&
result.content.encoding === "base64" &&
!ReadToolFileSystem.MEDIA_MIMES.has(result.content.mime)
)
return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource: result.target.resource }))
return { output: result.content, path: result.path }
}).pipe(
Effect.map((result) => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/tool/plugin/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export const Plugin = {
.create({
parentID: context.sessionID,
title: input.description,
agent: Agent.ID.make(input.agent),
agent: agent.id,
model,
})
.pipe(
Expand Down
22 changes: 0 additions & 22 deletions packages/core/test/tool-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,26 +933,4 @@ describe("ReadTool", () => {
])
}),
)

it.effect("rejects unsupported binary discovered by a direct read", () =>
Effect.gen(function* () {
readResult = {
type: "file",
uri: "file:///late-binary",
name: "late-binary",
content: "AAECAw==",
encoding: "base64",
mime: "application/octet-stream",
}
const registry = yield* Tool.Service

expect(
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-direct-binary", name: "read", input: { path: "late-binary" } },
}),
).toEqual({ status: "error", error: { type: "unknown", message: "Cannot read binary file: late-binary" } })
}),
)
})
Loading