feat(core): scaffold MCP service
This commit is contained in:
parent
0f272d931d
commit
af7ae326a3
3 changed files with 352 additions and 0 deletions
129
packages/core/test/mcp.test.ts
Normal file
129
packages/core/test/mcp.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { MCP } from "@opencode-ai/core/mcp"
|
||||
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
|
||||
function config(...values: unknown[]) {
|
||||
return Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed(
|
||||
values.map(
|
||||
(value, index) =>
|
||||
new Config.Document({ type: "document", path: `/config/${index}.json`, info: decode(value) }),
|
||||
),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function connector(closed: string[]) {
|
||||
return Layer.succeed(
|
||||
MCP.Connector,
|
||||
MCP.Connector.of({
|
||||
connect: (input) =>
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
closed.push(input.name)
|
||||
}),
|
||||
)
|
||||
return { name: input.name }
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function layer(closed: string[] = []) {
|
||||
return MCP.layer.pipe(
|
||||
Layer.provide(connector(closed)),
|
||||
Layer.provide(
|
||||
config(
|
||||
{
|
||||
mcp: {
|
||||
timeout: { startup: 10_000, request: 60_000 },
|
||||
servers: {
|
||||
local: { type: "local", command: ["node", "server.js"] },
|
||||
disabled: { type: "remote", url: "https://example.com/mcp", disabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
mcp: {
|
||||
timeout: { request: 120_000 },
|
||||
servers: {
|
||||
local: { type: "local", command: ["bun", "server.ts"], timeout: { startup: 20_000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const it = testEffect(layer())
|
||||
|
||||
describe("MCP", () => {
|
||||
it.effect("materializes location configuration and timeout overrides", () =>
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
|
||||
const servers = yield* mcp.list()
|
||||
expect(servers.map((server) => server.name)).toEqual(["local", "disabled"])
|
||||
expect(servers[0]?.config).toMatchObject({ type: "local", command: ["bun", "server.ts"] })
|
||||
expect(servers[0]?.timeout).toEqual(MCP.Timeout.make({ startup: 20_000, request: 120_000 }))
|
||||
expect(servers[0]?.status).toBeUndefined()
|
||||
expect(servers[1]?.timeout).toEqual(MCP.Timeout.make({ startup: 10_000, request: 120_000 }))
|
||||
expect(servers[1]?.status).toEqual(MCP.StatusDisabled.make({ status: "disabled" }))
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("owns connection scopes and status transitions", () =>
|
||||
Effect.gen(function* () {
|
||||
const closed: string[] = []
|
||||
const scope = yield* Scope.make()
|
||||
const context = yield* Layer.buildWithScope(Layer.fresh(layer(closed)), scope)
|
||||
const mcp = Context.get(context, MCP.Service)
|
||||
|
||||
yield* mcp.connect("local")
|
||||
expect((yield* mcp.get("local"))?.status).toEqual(MCP.StatusConnected.make({ status: "connected" }))
|
||||
|
||||
yield* mcp.disconnect("local")
|
||||
expect(closed).toEqual(["local"])
|
||||
expect((yield* mcp.get("local"))?.status).toEqual(MCP.StatusDisabled.make({ status: "disabled" }))
|
||||
|
||||
yield* mcp.connect("local")
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
expect(closed).toEqual(["local", "local"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("isolates connection state by location-layer instance", () =>
|
||||
Effect.gen(function* () {
|
||||
const firstScope = yield* Scope.make()
|
||||
const secondScope = yield* Scope.make()
|
||||
const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(layer()), firstScope), MCP.Service)
|
||||
const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(layer()), secondScope), MCP.Service)
|
||||
|
||||
yield* first.connect("local")
|
||||
expect((yield* first.get("local"))?.status?.status).toBe("connected")
|
||||
expect((yield* second.get("local"))?.status).toBeUndefined()
|
||||
|
||||
yield* Scope.close(firstScope, Exit.void)
|
||||
yield* Scope.close(secondScope, Exit.void)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects unknown and disabled servers", () =>
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
|
||||
expect(yield* mcp.connect("missing").pipe(Effect.flip)).toEqual(new MCP.NotFoundError({ name: "missing" }))
|
||||
expect(yield* mcp.connect("disabled").pipe(Effect.flip)).toEqual(new MCP.DisabledError({ name: "disabled" }))
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue