feat(core): add runtime MCP controls (#37308)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
parent
08a7080e11
commit
cd3cca0006
3 changed files with 280 additions and 71 deletions
|
|
@ -9,6 +9,10 @@ export const emptyMcpLayer = Layer.succeed(
|
|||
MCP.Service,
|
||||
MCP.Service.of({
|
||||
servers: () => Effect.succeed([]),
|
||||
add: () => Effect.die("unused mcp.add"),
|
||||
connect: () => Effect.die("unused mcp.connect"),
|
||||
disconnect: () => Effect.die("unused mcp.disconnect"),
|
||||
remove: () => Effect.die("unused mcp.remove"),
|
||||
tools: () => Effect.succeed([]),
|
||||
callTool: () => Effect.die("unused mcp.callTool"),
|
||||
instructions: () => Effect.succeed([]),
|
||||
|
|
|
|||
|
|
@ -148,7 +148,10 @@ function resourceServer(
|
|||
)
|
||||
}
|
||||
|
||||
function resourceMcpLayer(url: string, onFormCreated?: (form: Form.Info) => Effect.Effect<void>) {
|
||||
function resourceMcpLayer(
|
||||
server: string | typeof ConfigMCP.Server.Type,
|
||||
onFormCreated?: (form: Form.Info) => Effect.Effect<void>,
|
||||
) {
|
||||
const directory = AbsolutePath.make(import.meta.dir)
|
||||
const unusedIntegration = () => Effect.die("unused integration service")
|
||||
return MCP.layer.pipe(
|
||||
|
|
@ -164,7 +167,12 @@ function resourceMcpLayer(url: string, onFormCreated?: (form: Form.Info) => Effe
|
|||
type: "document",
|
||||
info: new Config.Info({
|
||||
mcp: new ConfigMCP.Info({
|
||||
servers: { resources: new ConfigMCP.Remote({ type: "remote", url, oauth: false }) },
|
||||
servers: {
|
||||
resources:
|
||||
typeof server === "string"
|
||||
? new ConfigMCP.Remote({ type: "remote", url: server, oauth: false })
|
||||
: server,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
|
@ -634,6 +642,120 @@ test("loads and reads MCP resources", async () => {
|
|||
)
|
||||
})
|
||||
|
||||
test("adds, disconnects, and reconnects MCP servers at runtime", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.gen(function* () {
|
||||
const service = yield* MCP.Service
|
||||
|
||||
expect((yield* service.servers())[0]?.status).toEqual({ status: "disabled" })
|
||||
expect(yield* service.connect("missing").pipe(Effect.flip)).toBeInstanceOf(MCP.NotFoundError)
|
||||
expect(yield* service.disconnect("missing").pipe(Effect.flip)).toBeInstanceOf(MCP.NotFoundError)
|
||||
yield* service.add(
|
||||
"dynamic",
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-output-schema.ts")],
|
||||
}),
|
||||
)
|
||||
expect((yield* service.servers()).find((server) => server.name === "dynamic")?.status).toEqual({
|
||||
status: "connected",
|
||||
})
|
||||
|
||||
yield* service.add(
|
||||
"dynamic",
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-output-schema.ts")],
|
||||
disabled: true,
|
||||
}),
|
||||
)
|
||||
expect((yield* service.servers()).find((server) => server.name === "dynamic")?.status).toEqual({
|
||||
status: "disabled",
|
||||
})
|
||||
expect(yield* service.tools()).toEqual([])
|
||||
|
||||
yield* service.connect("dynamic")
|
||||
expect((yield* service.servers()).find((server) => server.name === "dynamic")?.status).toEqual({
|
||||
status: "connected",
|
||||
})
|
||||
yield* service.disconnect("dynamic")
|
||||
expect((yield* service.servers()).find((server) => server.name === "dynamic")?.status).toEqual({
|
||||
status: "disabled",
|
||||
})
|
||||
expect(yield* service.tools()).toEqual([])
|
||||
|
||||
yield* service.connect("dynamic")
|
||||
expect((yield* service.servers()).find((server) => server.name === "dynamic")?.status).toEqual({
|
||||
status: "connected",
|
||||
})
|
||||
|
||||
yield* service.remove("dynamic")
|
||||
expect((yield* service.servers()).some((server) => server.name === "dynamic")).toBe(false)
|
||||
expect(yield* service.tools()).toEqual([])
|
||||
expect(yield* service.remove("dynamic").pipe(Effect.flip)).toBeInstanceOf(MCP.NotFoundError)
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
resourceMcpLayer(
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-output-schema.ts")],
|
||||
disabled: true,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
test("serializes concurrent MCP lifecycle operations", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* Effect.gen(function* () {
|
||||
const service = yield* MCP.Service
|
||||
|
||||
// Whatever order the racing operations land in, the resulting state must be consistent.
|
||||
yield* Effect.all(
|
||||
[
|
||||
service.connect("resources"),
|
||||
service.connect("resources"),
|
||||
service.disconnect("resources"),
|
||||
service.connect("resources"),
|
||||
],
|
||||
{ concurrency: "unbounded", discard: true },
|
||||
)
|
||||
const status = (yield* service.servers()).find((server) => server.name === "resources")?.status
|
||||
const tools = yield* service.tools()
|
||||
expect(status?.status === "connected" || status?.status === "disabled").toBe(true)
|
||||
if (status?.status === "disabled") expect(tools).toEqual([])
|
||||
if (status?.status === "connected") expect(tools.length).toBeGreaterThan(0)
|
||||
|
||||
yield* service.disconnect("resources")
|
||||
expect((yield* service.servers())[0]?.status).toEqual({ status: "disabled" })
|
||||
expect(yield* service.tools()).toEqual([])
|
||||
yield* service.connect("resources")
|
||||
expect((yield* service.servers())[0]?.status).toEqual({ status: "connected" })
|
||||
expect((yield* service.tools()).length).toBeGreaterThan(0)
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
resourceMcpLayer(
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-output-schema.ts")],
|
||||
disabled: true,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it.effect("advertises MCP output schemas to Code Mode", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue