feat(core): support mcp prompts

This commit is contained in:
Aiden Cline 2026-06-29 18:31:01 -05:00
commit f725b48508
2 changed files with 117 additions and 3 deletions

View file

@ -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<PromptArgument> | undefined
}
export interface PromptMessage {
readonly role: string
readonly content: unknown
}
export interface PromptResult {
readonly messages: ReadonlyArray<PromptMessage>
}
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<PromptDefinition[], Error>
/** Gets a server prompt; returns undefined when the server doesn't advertise prompt support. */
readonly prompt: (input: {
readonly name: string
readonly args?: Record<string, string>
}) => Effect.Effect<PromptResult | undefined, Error>
/** Lists the server's tools; returns [] when the server doesn't advertise tool support, fails on a transport error. */
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
/** 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 []

View file

@ -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