alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
208 lines (185 loc) • 5.71 kB
text/typescript
import {
$inject,
type Async,
createPrimitive,
KIND,
Primitive,
type Static,
type TObject,
z,
} from "alepha";
import type {
McpContext,
McpIcon,
McpPromptArgument,
McpPromptDescriptor,
PromptHandlerArgs,
PromptMessage,
} from "../interfaces/McpTypes.ts";
import { McpServerProvider } from "../providers/McpServerProvider.ts";
// ---------------------------------------------------------------------------------------------------------------------
/**
* Creates an MCP prompt primitive for defining reusable prompt templates.
*
* Prompts allow you to define templated messages that can be filled in
* with arguments at runtime. They're useful for creating consistent
* interaction patterns.
*
* @example
* ```ts
* class Prompts {
* greeting = $prompt({
* description: "Generate a personalized greeting",
* args: z.object({
* name: z.text({ description: "Name of the person to greet" }),
* style: z.enum(["formal", "casual"]).optional(),
* }),
* handler: async ({ args }) => [
* {
* role: "user",
* content: args.style === "formal"
* ? `Please greet ${args.name} in a formal manner.`
* : `Say hi to ${args.name}!`,
* },
* ],
* });
*
* codeReview = $prompt({
* description: "Request a code review",
* args: z.object({
* code: z.text({ description: "The code to review" }),
* language: z.text({ description: "Programming language" }),
* }),
* handler: async ({ args }) => [
* {
* role: "user",
* content: `Please review this ${args.language} code:\n\n${args.code}`,
* },
* ],
* });
* }
* ```
*/
export const $prompt = <T extends TObject>(
options: PromptPrimitiveOptions<T>,
): PromptPrimitive<T> => {
return createPrimitive(PromptPrimitive<T>, options);
};
// ---------------------------------------------------------------------------------------------------------------------
export interface PromptPrimitiveOptions<T extends TObject> {
/**
* The name of the prompt.
*
* If not provided, defaults to the property key where the prompt is declared.
*
* @example "greeting"
* @example "code-review"
*/
name?: string;
/**
* Human-friendly display title (spec 2025-11-25). Distinct from `name`,
* which remains the programmatic identifier.
*/
title?: string;
/**
* Description of what this prompt does.
*
* Helps users understand the purpose of the prompt.
*
* @example "Generate a personalized greeting message"
*/
description?: string;
/**
* Optional icons surfaced in client UIs (spec 2025-11-25 / SEP-973).
*/
icons?: McpIcon[];
/**
* TypeBox schema defining the prompt arguments.
*
* Each property in the schema becomes an argument that can be
* filled in when the prompt is used.
*/
args?: T;
/**
* Handler function that generates the prompt messages.
*
* Receives the validated arguments and returns an array of messages.
*
* @param args - Object containing validated arguments
* @returns Array of prompt messages
*/
handler: (args: PromptHandlerArgs<T>) => Async<PromptMessage[]>;
}
// ---------------------------------------------------------------------------------------------------------------------
export class PromptPrimitive<T extends TObject> extends Primitive<
PromptPrimitiveOptions<T>
> {
protected readonly mcpServer = $inject(McpServerProvider);
/**
* Returns the name of the prompt.
*/
public get name(): string {
return this.options.name ?? this.config.propertyKey;
}
/**
* Returns the description of the prompt.
*/
public get description(): string | undefined {
return this.options.description;
}
protected onInit(): void {
this.mcpServer.registerPrompt(this);
}
/**
* Get the prompt messages with the given arguments.
*
* @param rawArgs - Raw arguments to validate and pass to the handler
* @param context - Optional context from the transport layer
* @returns Array of prompt messages
*/
public async get(
rawArgs: unknown,
context?: McpContext,
): Promise<PromptMessage[]> {
let args = (rawArgs ?? {}) as Static<T>;
if (this.options.args) {
args = this.alepha.codec.decode(this.options.args, rawArgs ?? {});
}
return this.options.handler({ args, context });
}
/**
* Convert the prompt to an MCP prompt descriptor for protocol messages.
*/
public toDescriptor(): McpPromptDescriptor {
const descriptor: McpPromptDescriptor = {
name: this.name,
description: this.description,
arguments: this.options.args
? this.schemaToArguments(this.options.args)
: [],
};
if (this.options.title) descriptor.title = this.options.title;
if (this.options.icons && this.options.icons.length > 0) {
descriptor.icons = this.options.icons;
}
return descriptor;
}
/**
* Convert a TypeBox schema to an array of prompt arguments.
*/
protected schemaToArguments(schema: TObject): McpPromptArgument[] {
const args: McpPromptArgument[] = [];
for (const [name, propSchema] of Object.entries(schema.properties)) {
// The description lives on the inner schema; peel optional/nullable/default
// wrappers so `z.number({ description }).optional()` still surfaces it.
const prop = z.schema.unwrap(propSchema) as Record<string, unknown>;
args.push({
name,
description: prop.description as string | undefined,
required: !z.schema.isOptional(propSchema),
});
}
return args;
}
}
$prompt[KIND] = PromptPrimitive;