Skip to content

Commit 53bf3ed

Browse files
feat(runtime): add MCP prompts support (#2095)
Add support for MCP prompts in the runtime server, following the same patterns established for tools. Changes: - Add Prompt, PromptExecutionContext, CreatedPrompt types - Add createPrompt (requires auth) and createPublicPrompt factories - Extend CreateMCPServerOptions with prompts option - Register prompts with McpServer SDK (prompts/list, prompts/get) - Export prompt types from runtime package Bump version to 1.0.0-alpha.42
1 parent f2dc79f commit 53bf3ed

3 files changed

Lines changed: 151 additions & 3 deletions

File tree

packages/runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@decocms/runtime",
3-
"version": "1.0.0-alpha.41",
3+
"version": "1.0.0-alpha.42",
44
"type": "module",
55
"scripts": {
66
"check": "tsc --noEmit",

packages/runtime/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ import {
1414
type CreateMCPServerOptions,
1515
MCPServer,
1616
} from "./tools.ts";
17+
export {
18+
createPrompt,
19+
createPublicPrompt,
20+
type Prompt,
21+
type PromptArgsRawShape,
22+
type PromptExecutionContext,
23+
type CreatedPrompt,
24+
type GetPromptResult,
25+
} from "./tools.ts";
1726
import type { Binding } from "./wrangler.ts";
1827
export { proxyConnectionForId, BindingOf } from "./bindings.ts";
1928
export { type CORSOptions, type CORSOrigin } from "./cors.ts";

packages/runtime/src/tools.ts

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type EventBusBindingClient,
88
} from "@decocms/bindings";
99
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10+
import type { GetPromptResult } from "@modelcontextprotocol/sdk/types.js";
1011
import { z } from "zod";
1112
import { zodToJsonSchema } from "zod-to-json-schema";
1213
import { BindingRegistry } from "./bindings.ts";
@@ -81,6 +82,58 @@ export type CreatedTool = {
8182
}): Promise<unknown>;
8283
};
8384

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+
84137
/**
85138
* creates a private tool that always ensure for athentication before being executed
86139
*/
@@ -134,6 +187,43 @@ export function createTool<
134187
};
135188
}
136189

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+
137227
export interface ViewExport {
138228
title: string;
139229
icon: string;
@@ -260,6 +350,17 @@ export interface CreateMCPServerOptions<
260350
| Promise<CreatedTool[]>
261351
>
262352
| ((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[]>);
263364
}
264365

265366
export type Fetch<TEnv = unknown> = (
@@ -399,7 +500,7 @@ export const createMCPServer = <
399500

400501
const server = new McpServer(
401502
{ name: "@deco/mcp-api", version: "1.0.0" },
402-
{ capabilities: { tools: {} } },
503+
{ capabilities: { tools: {}, prompts: {} } },
403504
);
404505

405506
const toolsFn =
@@ -466,7 +567,45 @@ export const createMCPServer = <
466567
);
467568
}
468569

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 };
470609
};
471610

472611
const fetch = async (req: Request, env: TEnv) => {

0 commit comments

Comments
 (0)