From e4bc8b765b6b96dfcf3c836c412e6f2916df0459 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 28 Aug 2026 13:26:51 -0400 Subject: [PATCH] refactor(core): reuse MCP prompt helpers (#45660) --- packages/core/src/mcp/client.ts | 21 +++------------ packages/core/test/fixture/mcp-prompts.ts | 30 ++++++++++++++++++++++ packages/core/test/mcp.test.ts | 31 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 packages/core/test/fixture/mcp-prompts.ts diff --git a/packages/core/src/mcp/client.ts b/packages/core/src/mcp/client.ts index 9f58e44b4c9..c0f2fc94cc7 100644 --- a/packages/core/src/mcp/client.ts +++ b/packages/core/src/mcp/client.ts @@ -10,17 +10,14 @@ import { CallToolResultSchema, ElicitationCompleteNotificationSchema, ElicitRequestSchema, - GetPromptResultSchema, type Implementation, type ElicitRequestFormParams, type ElicitRequestParams, type ElicitRequestURLParams, type ElicitResult, - ListPromptsResultSchema, ListRootsRequestSchema, ListToolsResultSchema, PromptListChangedNotificationSchema, - PromptSchema, ResourceListChangedNotificationSchema, type LoggingMessageNotification, LoggingMessageNotificationSchema, @@ -41,10 +38,6 @@ const toError = (error: unknown) => (error instanceof Error ? error : new Error( const TolerantListToolsResult = ListToolsResultSchema.extend({ tools: ToolSchema.omit({ outputSchema: true }).array(), }) -const TolerantListPromptsResult = ListPromptsResultSchema.extend({ - prompts: PromptSchema.array(), -}) - export class NeedsAuthError extends Schema.TaggedError()("MCP.NeedsAuthError", { server: Schema.String, }) { @@ -301,12 +294,8 @@ export const connect = Effect.fnUntraced(function* ( const prompts = yield* Effect.tryPromise({ try: () => paginate( - async (cursor) => { - const params = cursor === undefined ? undefined : { cursor } - return client.request({ method: "prompts/list", params }, TolerantListPromptsResult, { - timeout: catalogTimeout, - }) - }, + (cursor) => + client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }), (result) => result.prompts, ), catch: toError, @@ -396,11 +385,7 @@ export const connect = Effect.fnUntraced(function* ( prompt: (input) => Effect.tryPromise({ try: (signal) => - client.request( - { method: "prompts/get", params: { name: input.name, arguments: input.args ?? {} } }, - GetPromptResultSchema, - { signal, timeout: executionTimeout }, - ), + client.getPrompt({ name: input.name, arguments: input.args ?? {} }, { signal, timeout: executionTimeout }), catch: toError, }).pipe( Effect.map((result) => ({ diff --git a/packages/core/test/fixture/mcp-prompts.ts b/packages/core/test/fixture/mcp-prompts.ts new file mode 100644 index 00000000000..473b6f176cd --- /dev/null +++ b/packages/core/test/fixture/mcp-prompts.ts @@ -0,0 +1,30 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { GetPromptRequestSchema, ListPromptsRequestSchema } from "@modelcontextprotocol/sdk/types.js" + +const server = new Server({ name: "prompts", version: "1.0.0" }, { capabilities: { prompts: {} } }) + +server.setRequestHandler(ListPromptsRequestSchema, ({ params }) => + Promise.resolve( + params?.cursor === "page-2" + ? { prompts: [{ name: "second", description: "Second prompt" }] } + : { + prompts: [ + { + name: "first", + description: "First prompt", + arguments: [{ name: "topic", description: "Topic to explain", required: true }], + }, + ], + nextCursor: "page-2", + }, + ), +) + +server.setRequestHandler(GetPromptRequestSchema, ({ params }) => + Promise.resolve({ + messages: [{ role: "user", content: { type: "text", text: params.arguments?.topic ?? "missing" } }], + }), +) + +await server.connect(new StdioServerTransport()) diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 959cc677473..79bcb36d0c0 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -474,6 +474,37 @@ test("retains output schemas across paginated MCP discovery", async () => { ]) }) +test("lists paginated prompts and invokes them through the MCP client", async () => { + const result = await Effect.runPromise( + Effect.scoped( + Effect.gen(function* () { + const connection = yield* connect( + "prompts", + new ConfigMCP.Local({ + type: "local", + command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-prompts.ts")], + }), + import.meta.dir, + ) + return { + prompts: yield* connection.prompts(), + result: yield* connection.prompt({ name: "first", args: { topic: "Effect" } }), + } + }), + ), + ) + + expect(result.prompts).toEqual([ + { + name: "first", + description: "First prompt", + arguments: [{ name: "topic", description: "Topic to explain", required: true }], + }, + { name: "second", description: "Second prompt", arguments: undefined }, + ]) + expect(result.result).toEqual({ messages: [{ role: "user", content: { type: "text", text: "Effect" } }] }) +}) + test("spawns local MCP servers through the location environment", async () => { const spawns: Array = [] const cwd = path.join(import.meta.dir, "fixture")