|
|
@@ -1,6 +1,7 @@
|
|
|
import path from "node:path"
|
|
|
+import { pathToFileURL } from "node:url"
|
|
|
import { expect, mock, beforeEach } from "bun:test"
|
|
|
-import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
|
|
|
+import { ListRootsRequestSchema, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
|
|
|
import { Cause, Effect, Exit } from "effect"
|
|
|
import type { MCP as MCPNS } from "../../src/mcp/index"
|
|
|
import { testEffect } from "../lib/effect"
|
|
|
@@ -38,6 +39,8 @@ interface MockClientState {
|
|
|
{ resources: Array<{ name: string; uri: string; description?: string }>; nextCursor?: string }
|
|
|
>
|
|
|
closed: boolean
|
|
|
+ clientOptions?: { capabilities?: { roots?: { listChanged?: boolean } } }
|
|
|
+ requestHandlers: Map<unknown, (...args: any[]) => Promise<any>>
|
|
|
notificationHandlers: Map<unknown, (...args: any[]) => any>
|
|
|
}
|
|
|
|
|
|
@@ -75,6 +78,7 @@ function getOrCreateClientState(name?: string): MockClientState {
|
|
|
promptPages: {},
|
|
|
resourcePages: {},
|
|
|
closed: false,
|
|
|
+ requestHandlers: new Map(),
|
|
|
notificationHandlers: new Map(),
|
|
|
}
|
|
|
clientStates.set(key, state)
|
|
|
@@ -149,8 +153,10 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
|
_state!: MockClientState
|
|
|
transport: any
|
|
|
|
|
|
- constructor(_opts: any) {
|
|
|
+ constructor(_info: any, options?: MockClientState["clientOptions"]) {
|
|
|
clientCreateCount++
|
|
|
+ this._state = getOrCreateClientState(lastCreatedClientName)
|
|
|
+ this._state.clientOptions = options
|
|
|
}
|
|
|
|
|
|
async connect(transport: { start: () => Promise<void> }) {
|
|
|
@@ -160,6 +166,10 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|
|
this._state = getOrCreateClientState(lastCreatedClientName)
|
|
|
}
|
|
|
|
|
|
+ setRequestHandler(schema: unknown, handler: (...args: any[]) => Promise<any>) {
|
|
|
+ this._state.requestHandlers.set(schema, handler)
|
|
|
+ }
|
|
|
+
|
|
|
setNotificationHandler(schema: unknown, handler: (...args: any[]) => any) {
|
|
|
this._state?.notificationHandlers.set(schema, handler)
|
|
|
}
|
|
|
@@ -251,6 +261,28 @@ function statusName(status: Record<string, MCPNS.Status> | MCPNS.Status, server:
|
|
|
return status[server]?.status
|
|
|
}
|
|
|
|
|
|
+it.instance(
|
|
|
+ "advertises and lists the instance directory as its root",
|
|
|
+ () =>
|
|
|
+ MCP.Service.use((mcp: MCPNS.Interface) =>
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const { directory } = yield* TestInstance
|
|
|
+ lastCreatedClientName = "roots"
|
|
|
+ yield* mcp.add("roots", { type: "local", command: ["echo", "test"] })
|
|
|
+
|
|
|
+ const state = getOrCreateClientState("roots")
|
|
|
+ expect(state.clientOptions?.capabilities?.roots).toEqual({})
|
|
|
+ expect(state.clientOptions?.capabilities?.roots?.listChanged).toBeUndefined()
|
|
|
+
|
|
|
+ const handler = state.requestHandlers.get(ListRootsRequestSchema)
|
|
|
+ expect(handler).toBeDefined()
|
|
|
+ const result = yield* Effect.promise(() => handler?.() ?? Promise.reject(new Error("roots handler missing")))
|
|
|
+ expect(result).toEqual({ roots: [{ uri: pathToFileURL(directory).href }] })
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ { config: { mcp: {} } },
|
|
|
+)
|
|
|
+
|
|
|
it.instance(
|
|
|
"local mcp cwd resolves relative paths against instance directory",
|
|
|
() =>
|