feat(mcp): expose resource APIs
This commit is contained in:
parent
ab5bd520e2
commit
e1d1352d8f
18 changed files with 301 additions and 17 deletions
|
|
@ -439,8 +439,28 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
|
|||
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
|
||||
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
|
||||
|
||||
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
|
||||
export type ServerMcpResourceCatalogOperation<E = never> = (
|
||||
input?: Endpoint10_1Input,
|
||||
) => Effect.Effect<Endpoint10_1Output, E>
|
||||
|
||||
type Endpoint10_2Request = Parameters<RawClient["server.mcp"]["mcp.resource.read"]>[0]
|
||||
export type Endpoint10_2Input = {
|
||||
readonly location?: Endpoint10_2Request["query"]["location"]
|
||||
readonly server: Endpoint10_2Request["payload"]["server"]
|
||||
readonly uri: Endpoint10_2Request["payload"]["uri"]
|
||||
}
|
||||
export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.read"]>>
|
||||
export type ServerMcpReadResourceOperation<E = never> = (
|
||||
input: Endpoint10_2Input,
|
||||
) => Effect.Effect<Endpoint10_2Output, E>
|
||||
|
||||
export interface ServerMcpApi<E = never> {
|
||||
readonly list: ServerMcpListOperation<E>
|
||||
readonly resourceCatalog: ServerMcpResourceCatalogOperation<E>
|
||||
readonly readResource: ServerMcpReadResourceOperation<E>
|
||||
}
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
|
|
|
|||
|
|
@ -529,7 +529,28 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
|
|||
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
|
||||
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) })
|
||||
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
|
||||
type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
|
||||
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
|
||||
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint10_2Request = Parameters<RawClient["server.mcp"]["mcp.resource.read"]>[0]
|
||||
type Endpoint10_2Input = {
|
||||
readonly location?: Endpoint10_2Request["query"]["location"]
|
||||
readonly server: Endpoint10_2Request["payload"]["server"]
|
||||
readonly uri: Endpoint10_2Request["payload"]["uri"]
|
||||
}
|
||||
const Endpoint10_2 = (raw: RawClient["server.mcp"]) => (input: Endpoint10_2Input) =>
|
||||
raw["mcp.resource.read"]({
|
||||
query: { location: input["location"] },
|
||||
payload: { server: input["server"], uri: input["uri"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({
|
||||
list: Endpoint10_0(raw),
|
||||
resourceCatalog: Endpoint10_1(raw),
|
||||
readResource: Endpoint10_2(raw),
|
||||
})
|
||||
|
||||
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
|
||||
type Endpoint11_0Input = {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export type {
|
|||
ProviderApi,
|
||||
ReferenceApi,
|
||||
SessionApi,
|
||||
ServerMcpApi,
|
||||
SkillApi,
|
||||
} from "./api.js"
|
||||
export { Service } from "./service.js"
|
||||
|
|
@ -27,6 +28,7 @@ export { FileSystem } from "@opencode-ai/schema/filesystem"
|
|||
export { Form } from "@opencode-ai/schema/form"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Location } from "@opencode-ai/schema/location"
|
||||
export { Mcp } from "@opencode-ai/schema/mcp"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Permission } from "@opencode-ai/schema/permission"
|
||||
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
ProviderApi as EffectProviderApi,
|
||||
ReferenceApi as EffectReferenceApi,
|
||||
SessionApi as EffectSessionApi,
|
||||
ServerMcpApi as EffectServerMcpApi,
|
||||
SkillApi as EffectSkillApi,
|
||||
} from "../effect/api/api.js"
|
||||
import type { Effect, Stream } from "effect"
|
||||
|
|
@ -37,6 +38,7 @@ export type PluginApi = PromisifyApi<EffectPluginApi<unknown>>
|
|||
export type ProviderApi = PromisifyApi<EffectProviderApi<unknown>>
|
||||
export type ReferenceApi = PromisifyApi<EffectReferenceApi<unknown>>
|
||||
export type SessionApi = PromisifyApi<EffectSessionApi<unknown>>
|
||||
export type ServerMcpApi = PromisifyApi<EffectServerMcpApi<unknown>>
|
||||
export type SkillApi = PromisifyApi<EffectSkillApi<unknown>>
|
||||
|
||||
export interface CatalogApi {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ import type {
|
|||
IntegrationAttemptCancelOutput,
|
||||
ServerMcpListInput,
|
||||
ServerMcpListOutput,
|
||||
ServerMcpResourceCatalogInput,
|
||||
ServerMcpResourceCatalogOutput,
|
||||
ServerMcpReadResourceInput,
|
||||
ServerMcpReadResourceOutput,
|
||||
CredentialUpdateInput,
|
||||
CredentialUpdateOutput,
|
||||
CredentialRemoveInput,
|
||||
|
|
@ -890,6 +894,31 @@ export function make(options: ClientOptions) {
|
|||
},
|
||||
requestOptions,
|
||||
),
|
||||
resourceCatalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
|
||||
request<ServerMcpResourceCatalogOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/mcp/resource`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
readResource: (input: ServerMcpReadResourceInput, requestOptions?: RequestOptions) =>
|
||||
request<ServerMcpReadResourceOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/mcp/resource/read`,
|
||||
query: { location: input["location"] },
|
||||
body: { server: input["server"], uri: input["uri"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
credential: {
|
||||
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,14 @@ export type ProviderNotFoundError = {
|
|||
export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError"
|
||||
|
||||
export type McpServerNotFoundError = {
|
||||
readonly _tag: "McpServerNotFoundError"
|
||||
readonly server: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isMcpServerNotFoundError = (value: unknown): value is McpServerNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "McpServerNotFoundError"
|
||||
|
||||
export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string }
|
||||
export const isFormNotFoundError = (value: unknown): value is FormNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError"
|
||||
|
|
@ -2478,6 +2486,60 @@ export type ServerMcpListOutput = {
|
|||
}>
|
||||
}
|
||||
|
||||
export type ServerMcpResourceCatalogInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type ServerMcpResourceCatalogOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly project: { readonly id: string; readonly directory: string }
|
||||
}
|
||||
readonly data: {
|
||||
readonly resources: ReadonlyArray<{
|
||||
readonly server: string
|
||||
readonly name: string
|
||||
readonly uri: string
|
||||
readonly description?: string
|
||||
readonly mimeType?: string
|
||||
}>
|
||||
readonly templates: ReadonlyArray<{
|
||||
readonly server: string
|
||||
readonly name: string
|
||||
readonly uriTemplate: string
|
||||
readonly description?: string
|
||||
readonly mimeType?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerMcpReadResourceInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
readonly server: { readonly server: string; readonly uri: string }["server"]
|
||||
readonly uri: { readonly server: string; readonly uri: string }["uri"]
|
||||
}
|
||||
|
||||
export type ServerMcpReadResourceOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly project: { readonly id: string; readonly directory: string }
|
||||
}
|
||||
readonly data: {
|
||||
readonly server: string
|
||||
readonly uri: string
|
||||
readonly contents: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly uri: string; readonly text: string; readonly mimeType?: string }
|
||||
| { readonly type: "blob"; readonly uri: string; readonly blob: string; readonly mimeType?: string }
|
||||
>
|
||||
} | null
|
||||
}
|
||||
|
||||
export type CredentialUpdateInput = {
|
||||
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
||||
readonly location?: {
|
||||
|
|
@ -5508,6 +5570,14 @@ export type EventSubscribeOutput =
|
|||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly server: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly created: number
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "mcp.resources.changed"
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly server: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly created: number
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export type {
|
|||
ProviderApi,
|
||||
ReferenceApi,
|
||||
SessionApi,
|
||||
ServerMcpApi,
|
||||
SkillApi,
|
||||
} from "./api.js"
|
||||
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/inpu
|
|||
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
|
|
@ -26,6 +27,7 @@ const Client = await import("../src/effect")
|
|||
test("effect entrypoint exposes canonical Schema contracts", () => {
|
||||
expect(Client.Agent).toBe(Agent)
|
||||
expect(Client.Model).toBe(Model)
|
||||
expect(Client.Mcp).toBe(Mcp)
|
||||
expect(Client.Session).toBe(Session)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src/promise/index"
|
||||
import { isMcpServerNotFoundError, isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src/promise/index"
|
||||
|
||||
test("exposes every standard HTTP API group", () => {
|
||||
const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||
|
|
@ -48,6 +48,74 @@ test("exposes every standard HTTP API group", () => {
|
|||
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
|
||||
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
|
||||
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
|
||||
expect(Object.keys(client["server.mcp"])).toEqual(["list", "resourceCatalog", "readResource"])
|
||||
})
|
||||
|
||||
test("MCP resource methods use the public HTTP contract", async () => {
|
||||
const requests: Array<{ method: string; url: string; body?: unknown }> = []
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (input, init) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
requests.push({
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
body: request.method === "POST" ? await request.json() : undefined,
|
||||
})
|
||||
if (request.method === "POST")
|
||||
return Response.json({
|
||||
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
|
||||
data: {
|
||||
server: "docs",
|
||||
uri: "docs://readme",
|
||||
contents: [{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" }],
|
||||
},
|
||||
})
|
||||
return Response.json({
|
||||
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
|
||||
data: {
|
||||
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
|
||||
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const location = { directory: "/tmp/project" }
|
||||
expect((await client["server.mcp"].resourceCatalog({ location })).data.resources[0]?.uri).toBe("docs://readme")
|
||||
expect(
|
||||
(await client["server.mcp"].readResource({ server: "docs", uri: "docs://readme", location })).data?.contents,
|
||||
).toEqual([{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" }])
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
method: "GET",
|
||||
url: "http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject",
|
||||
body: undefined,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "http://localhost:3000/api/mcp/resource/read?location%5Bdirectory%5D=%2Ftmp%2Fproject",
|
||||
body: { server: "docs", uri: "docs://readme" },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("MCP resource reads preserve unknown-server errors", async () => {
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async () =>
|
||||
Response.json(
|
||||
{ _tag: "McpServerNotFoundError", server: "missing", message: "MCP server not found: missing" },
|
||||
{ status: 404 },
|
||||
),
|
||||
})
|
||||
|
||||
try {
|
||||
await client["server.mcp"].readResource({ server: "missing", uri: "docs://readme" })
|
||||
throw new Error("Expected request to fail")
|
||||
} catch (error) {
|
||||
expect(isMcpServerNotFoundError(error)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("file.read returns binary content from the public HTTP contract", async () => {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ export const groupNames = {
|
|||
} as const
|
||||
|
||||
export const endpointNames = {
|
||||
"mcp.resource.catalog": "resourceCatalog",
|
||||
"mcp.resource.read": "readResource",
|
||||
"session.messages": "list",
|
||||
"integration.connect.key": "connectKey",
|
||||
"integration.connect.oauth": "connectOauth",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,15 @@ export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFo
|
|||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class McpServerNotFoundError extends Schema.TaggedErrorClass<McpServerNotFoundError>()(
|
||||
"McpServerNotFoundError",
|
||||
{
|
||||
server: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class SessionNotFoundError extends Schema.TaggedErrorClass<SessionNotFoundError>()(
|
||||
"SessionNotFoundError",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Mcp } from "@opencode-ai/schema/mcp"
|
|||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { McpServerNotFoundError } from "../errors.js"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
|
||||
export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
|
|
@ -19,4 +20,34 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
|||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server status routes." }))
|
||||
.add(
|
||||
HttpApiEndpoint.get("mcp.resource.catalog", "/api/mcp/resource", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Mcp.ResourceCatalog),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.mcp.resource.catalog",
|
||||
summary: "List MCP resources",
|
||||
description: "Retrieve resources and resource templates from connected MCP servers.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("mcp.resource.read", "/api/mcp/resource/read", {
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ server: Schema.String, uri: Schema.String }),
|
||||
success: Location.response(Schema.NullOr(Mcp.ResourceContent)),
|
||||
error: McpServerNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.mcp.resource.read",
|
||||
summary: "Read MCP resource",
|
||||
description: "Read the current content of one resource from a connected MCP server.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "mcp", description: "MCP server and resource routes." }))
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ import { isOpenCodeEvent } from "../src/groups/event.js"
|
|||
test("classifies public events by type", () => {
|
||||
expect(isOpenCodeEvent({ type: "server.connected" })).toBe(true)
|
||||
expect(isOpenCodeEvent({ type: "mcp.status.changed" })).toBe(true)
|
||||
expect(isOpenCodeEvent({ type: "mcp.resources.changed" })).toBe(true)
|
||||
expect(isOpenCodeEvent({ type: "mcp.tools.changed" })).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export const ServerDefinitions = Event.inventory(
|
|||
...InstallationEvent.Definitions,
|
||||
...VcsEvent.Definitions,
|
||||
McpEvent.StatusChanged,
|
||||
McpEvent.ResourcesChanged,
|
||||
// Shared transitional: V1 contracts the current TUI still consumes during
|
||||
// the migration (permission.asked/replied, question.asked, session.error).
|
||||
// Remove when the TUI moves to the current permission/question surfaces.
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ describe("public event manifest", () => {
|
|||
expect(EventManifest.Server.get("mcp.status.changed")).toBe(McpEvent.StatusChanged)
|
||||
expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted)
|
||||
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
|
||||
expect(EventManifest.Server.has("mcp.resources.changed")).toBe(false)
|
||||
expect(EventManifest.Server.get("mcp.resources.changed")).toBe(McpEvent.ResourcesChanged)
|
||||
expect(Agent.Event.Updated.durable).toBeUndefined()
|
||||
expect(EventManifest.Durable.has("agent.updated")).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export { Credential } from "@opencode-ai/schema/credential"
|
|||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Location } from "@opencode-ai/schema/location"
|
||||
export { Mcp } from "@opencode-ai/schema/mcp"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Permission } from "@opencode-ai/schema/permission"
|
||||
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
|
||||
const SDK = await import("../src/index")
|
||||
|
|
@ -8,6 +9,7 @@ const SDK = await import("../src/index")
|
|||
test("re-exports canonical contracts directly from Schema", () => {
|
||||
expect(SDK.Agent).toBe(Agent)
|
||||
expect(SDK.Model).toBe(Model)
|
||||
expect(SDK.Mcp).toBe(Mcp)
|
||||
expect(SDK.Session).toBe(Session)
|
||||
expect(Object.keys(SDK).sort()).toEqual([
|
||||
"AbsolutePath",
|
||||
|
|
@ -18,6 +20,7 @@ test("re-exports canonical contracts directly from Schema", () => {
|
|||
"FileSystem",
|
||||
"Integration",
|
||||
"Location",
|
||||
"Mcp",
|
||||
"Model",
|
||||
"OpenCode",
|
||||
"Permission",
|
||||
|
|
|
|||
|
|
@ -2,24 +2,45 @@ import { MCP } from "@opencode-ai/core/mcp/index"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { McpServerNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { response } from "../location"
|
||||
|
||||
export const McpHandler = HttpApiBuilder.group(Api, "server.mcp", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"mcp.list",
|
||||
Effect.fn(function* () {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(
|
||||
service
|
||||
.servers()
|
||||
.pipe(
|
||||
Effect.map((servers) =>
|
||||
servers.map((info) => ({ name: info.name, status: info.status, integrationID: info.integrationID })),
|
||||
return handlers
|
||||
.handle(
|
||||
"mcp.list",
|
||||
Effect.fn(function* () {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(
|
||||
service
|
||||
.servers()
|
||||
.pipe(
|
||||
Effect.map((servers) =>
|
||||
servers.map((info) => ({ name: info.name, status: info.status, integrationID: info.integrationID })),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"mcp.resource.catalog",
|
||||
Effect.fn(function* () {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(service.resourceCatalog())
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"mcp.resource.read",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* MCP.Service
|
||||
return yield* response(
|
||||
service.readResource({ server: ctx.payload.server, uri: ctx.payload.uri }).pipe(
|
||||
Effect.map((result) => result ?? null),
|
||||
Effect.mapError((error) => new McpServerNotFoundError({ server: error.server, message: error.message })),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue