|
7 | 7 | type EventBusBindingClient, |
8 | 8 | } from "@decocms/bindings"; |
9 | 9 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 10 | +import type { GetPromptResult } from "@modelcontextprotocol/sdk/types.js"; |
10 | 11 | import { z } from "zod"; |
11 | 12 | import { zodToJsonSchema } from "zod-to-json-schema"; |
12 | 13 | import { BindingRegistry } from "./bindings.ts"; |
@@ -81,6 +82,58 @@ export type CreatedTool = { |
81 | 82 | }): Promise<unknown>; |
82 | 83 | }; |
83 | 84 |
|
| 85 | +// Re-export GetPromptResult for external use |
| 86 | +export type { GetPromptResult } from "@modelcontextprotocol/sdk/types.js"; |
| 87 | + |
| 88 | +/** |
| 89 | + * Prompt argument schema shape - must be string types per MCP specification. |
| 90 | + * Unlike tool arguments, prompt arguments are always strings. |
| 91 | + */ |
| 92 | +export type PromptArgsRawShape = { |
| 93 | + [k: string]: |
| 94 | + | z.ZodType<string, z.ZodTypeDef, string> |
| 95 | + | z.ZodOptional<z.ZodType<string, z.ZodTypeDef, string>>; |
| 96 | +}; |
| 97 | + |
| 98 | +/** |
| 99 | + * Context passed to prompt execute functions. |
| 100 | + */ |
| 101 | +export interface PromptExecutionContext< |
| 102 | + TArgs extends PromptArgsRawShape = PromptArgsRawShape, |
| 103 | +> { |
| 104 | + args: z.objectOutputType<TArgs, z.ZodTypeAny>; |
| 105 | + runtimeContext: AppContext; |
| 106 | +} |
| 107 | + |
| 108 | +/** |
| 109 | + * Prompt interface with generic argument types for type-safe prompt creation. |
| 110 | + */ |
| 111 | +export interface Prompt<TArgs extends PromptArgsRawShape = PromptArgsRawShape> { |
| 112 | + name: string; |
| 113 | + title?: string; |
| 114 | + description?: string; |
| 115 | + argsSchema?: TArgs; |
| 116 | + execute( |
| 117 | + context: PromptExecutionContext<TArgs>, |
| 118 | + ): Promise<GetPromptResult> | GetPromptResult; |
| 119 | +} |
| 120 | + |
| 121 | +/** |
| 122 | + * CreatedPrompt is a permissive type that any Prompt can be assigned to. |
| 123 | + * Uses a structural type with relaxed execute signature to allow prompts with any schema. |
| 124 | + */ |
| 125 | +export type CreatedPrompt = { |
| 126 | + name: string; |
| 127 | + title?: string; |
| 128 | + description?: string; |
| 129 | + argsSchema?: PromptArgsRawShape; |
| 130 | + // Use a permissive execute signature - accepts any args shape |
| 131 | + execute(context: { |
| 132 | + args: Record<string, string | undefined>; |
| 133 | + runtimeContext: AppContext; |
| 134 | + }): Promise<GetPromptResult> | GetPromptResult; |
| 135 | +}; |
| 136 | + |
84 | 137 | /** |
85 | 138 | * creates a private tool that always ensure for athentication before being executed |
86 | 139 | */ |
@@ -134,6 +187,43 @@ export function createTool< |
134 | 187 | }; |
135 | 188 | } |
136 | 189 |
|
| 190 | +/** |
| 191 | + * Creates a public prompt that does not require authentication. |
| 192 | + */ |
| 193 | +export function createPublicPrompt<TArgs extends PromptArgsRawShape>( |
| 194 | + opts: Prompt<TArgs>, |
| 195 | +): Prompt<TArgs> { |
| 196 | + return { |
| 197 | + ...opts, |
| 198 | + execute: (input: PromptExecutionContext<TArgs>) => { |
| 199 | + return opts.execute({ |
| 200 | + ...input, |
| 201 | + runtimeContext: createRuntimeContext(input.runtimeContext), |
| 202 | + }); |
| 203 | + }, |
| 204 | + }; |
| 205 | +} |
| 206 | + |
| 207 | +/** |
| 208 | + * Creates a prompt that always ensures authentication before being executed. |
| 209 | + * This is the default and recommended way to create prompts. |
| 210 | + */ |
| 211 | +export function createPrompt<TArgs extends PromptArgsRawShape>( |
| 212 | + opts: Prompt<TArgs>, |
| 213 | +): Prompt<TArgs> { |
| 214 | + const execute = opts.execute; |
| 215 | + return createPublicPrompt({ |
| 216 | + ...opts, |
| 217 | + execute: (input: PromptExecutionContext<TArgs>) => { |
| 218 | + const env = input.runtimeContext.env; |
| 219 | + if (env) { |
| 220 | + env.MESH_REQUEST_CONTEXT?.ensureAuthenticated(); |
| 221 | + } |
| 222 | + return execute(input); |
| 223 | + }, |
| 224 | + }); |
| 225 | +} |
| 226 | + |
137 | 227 | export interface ViewExport { |
138 | 228 | title: string; |
139 | 229 | icon: string; |
@@ -260,6 +350,17 @@ export interface CreateMCPServerOptions< |
260 | 350 | | Promise<CreatedTool[]> |
261 | 351 | > |
262 | 352 | | ((env: TEnv) => CreatedTool[] | Promise<CreatedTool[]>); |
| 353 | + prompts?: |
| 354 | + | Array< |
| 355 | + ( |
| 356 | + env: TEnv, |
| 357 | + ) => |
| 358 | + | Promise<CreatedPrompt> |
| 359 | + | CreatedPrompt |
| 360 | + | CreatedPrompt[] |
| 361 | + | Promise<CreatedPrompt[]> |
| 362 | + > |
| 363 | + | ((env: TEnv) => CreatedPrompt[] | Promise<CreatedPrompt[]>); |
263 | 364 | } |
264 | 365 |
|
265 | 366 | export type Fetch<TEnv = unknown> = ( |
@@ -399,7 +500,7 @@ export const createMCPServer = < |
399 | 500 |
|
400 | 501 | const server = new McpServer( |
401 | 502 | { name: "@deco/mcp-api", version: "1.0.0" }, |
402 | | - { capabilities: { tools: {} } }, |
| 503 | + { capabilities: { tools: {}, prompts: {} } }, |
403 | 504 | ); |
404 | 505 |
|
405 | 506 | const toolsFn = |
@@ -466,7 +567,45 @@ export const createMCPServer = < |
466 | 567 | ); |
467 | 568 | } |
468 | 569 |
|
469 | | - return { server, tools }; |
| 570 | + // Resolve and register prompts |
| 571 | + const promptsFn = |
| 572 | + typeof options.prompts === "function" |
| 573 | + ? options.prompts |
| 574 | + : async (bindings: TEnv) => { |
| 575 | + if (typeof options.prompts === "function") { |
| 576 | + return await options.prompts(bindings); |
| 577 | + } |
| 578 | + return await Promise.all( |
| 579 | + options.prompts?.flatMap(async (prompt) => { |
| 580 | + const promptResult = prompt(bindings); |
| 581 | + const awaited = await promptResult; |
| 582 | + if (Array.isArray(awaited)) { |
| 583 | + return awaited; |
| 584 | + } |
| 585 | + return [awaited]; |
| 586 | + }) ?? [], |
| 587 | + ).then((p) => p.flat()); |
| 588 | + }; |
| 589 | + const prompts = await promptsFn(bindings); |
| 590 | + |
| 591 | + for (const prompt of prompts) { |
| 592 | + server.registerPrompt( |
| 593 | + prompt.name, |
| 594 | + { |
| 595 | + title: prompt.title, |
| 596 | + description: prompt.description, |
| 597 | + argsSchema: prompt.argsSchema, |
| 598 | + }, |
| 599 | + async (args) => { |
| 600 | + return await prompt.execute({ |
| 601 | + args: args as Record<string, string | undefined>, |
| 602 | + runtimeContext: createRuntimeContext(), |
| 603 | + }); |
| 604 | + }, |
| 605 | + ); |
| 606 | + } |
| 607 | + |
| 608 | + return { server, tools, prompts }; |
470 | 609 | }; |
471 | 610 |
|
472 | 611 | const fetch = async (req: Request, env: TEnv) => { |
|
0 commit comments