diff --git a/packages/core/src/mcp/client.ts b/packages/core/src/mcp/client.ts index 938ee86afc..b8e9b10767 100644 --- a/packages/core/src/mcp/client.ts +++ b/packages/core/src/mcp/client.ts @@ -46,6 +46,27 @@ export interface ToolDefinition { readonly inputSchema: unknown } +export interface PromptArgument { + readonly name: string + readonly description: string | undefined + readonly required: boolean | undefined +} + +export interface PromptDefinition { + readonly name: string + readonly description: string | undefined + readonly arguments: ReadonlyArray | undefined +} + +export interface PromptMessage { + readonly role: string + readonly content: unknown +} + +export interface PromptResult { + readonly messages: ReadonlyArray +} + export type CallToolContent = | { readonly type: "text"; readonly text: string } | { readonly type: "media"; readonly data: string; readonly mimeType: string } @@ -66,6 +87,13 @@ export interface LogMessage { export interface Connection { /** Server-supplied usage instructions from the initialize result, if any. */ readonly instructions: string | undefined + /** Lists the server's prompts; returns [] when the server doesn't advertise prompt support. */ + readonly prompts: () => Effect.Effect + /** Gets a server prompt; returns undefined when the server doesn't advertise prompt support. */ + readonly prompt: (input: { + readonly name: string + readonly args?: Record + }) => Effect.Effect /** Lists the server's tools; returns [] when the server doesn't advertise tool support, fails on a transport error. */ readonly tools: () => Effect.Effect /** Invokes a tool on the server. Interruption aborts the in-flight request. */ @@ -133,6 +161,42 @@ export const connect = Effect.fnUntraced(function* ( const requestTimeout = config.timeout?.request ?? DEFAULT_REQUEST_TIMEOUT return { instructions: client.getInstructions()?.trim() || undefined, + prompts: () => + Effect.gen(function* () { + if (!client.getServerCapabilities()?.prompts) return [] + const prompts = yield* Effect.tryPromise({ + try: () => + paginate( + (cursor) => + client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout: requestTimeout }), + (result) => result.prompts, + ), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }).pipe( + Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })), + ) + return prompts.map((prompt) => ({ + name: prompt.name, + description: prompt.description, + arguments: prompt.arguments?.map((argument) => ({ + name: argument.name, + description: argument.description, + required: argument.required, + })), + })) + }), + prompt: (input) => + Effect.gen(function* () { + if (!client.getServerCapabilities()?.prompts) return undefined + const result = yield* Effect.tryPromise({ + try: (signal) => + client.getPrompt({ name: input.name, arguments: input.args }, { timeout: requestTimeout, signal }), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }) + return { + messages: result.messages.map((message) => ({ role: message.role, content: message.content })), + } + }), tools: () => Effect.gen(function* () { if (!client.getServerCapabilities()?.tools) return [] diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index b037714328..2f21d075aa 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -237,6 +237,21 @@ export const layer = Layer.effect( const toTool = (server: ServerName, def: MCPClient.ToolDefinition) => new Tool({ server, name: def.name, description: def.description, inputSchema: def.inputSchema }) + const toPrompt = (server: ServerName, def: MCPClient.PromptDefinition) => + new Prompt({ + server, + name: def.name, + description: def.description, + arguments: def.arguments?.map( + (argument) => + new PromptArgument({ + name: argument.name, + description: argument.description, + required: argument.required, + }), + ), + }) + const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => connection.tools().pipe( Effect.map((defs) => { @@ -372,11 +387,46 @@ export const layer = Layer.effect( }), prompts: Effect.fn("MCP.prompts")(function* () { yield* whenAllReady - return [] + return yield* Effect.forEach( + Array.from(runtime), + ([server, entry]) => { + if (!entry.client || entry.status.status !== "connected") return Effect.succeed([]) + return entry.client.prompts().pipe( + Effect.map((defs) => defs.map((def) => toPrompt(server, def))), + Effect.orElseSucceed(() => []), + ) + }, + { concurrency: "unbounded" }, + ).pipe( + Effect.map((items) => + items.flat().toSorted((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name)), + ), + ) }), prompt: Effect.fn("MCP.prompt")(function* (input) { - yield* gate(input.server) - return undefined + const target = yield* requireServer(input.server) + yield* Deferred.await(target.entry.startup) + if (!target.entry.client) return undefined + const result = yield* target.entry.client + .prompt({ name: input.name, args: input.args }) + .pipe( + Effect.tapError((error) => + Effect.logError("failed to get MCP prompt", { + server: target.name, + prompt: input.name, + error: error.message, + }), + ), + Effect.orElseSucceed(() => undefined), + ) + if (!result) return undefined + return new PromptResult({ + server: target.name, + name: input.name, + messages: result.messages.map( + (message) => new PromptMessage({ role: message.role, content: message.content }), + ), + }) }), resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () { yield* whenAllReady