MCP Fusion/Protocol and runtime/Prompts engine
Prompts engine
Prompts are first-class citizens beside tools: typed argument schemas, loopback tool execution from prompt handlers, pagination with signed cursors, interceptors and hydration deadlines.
Tools execute; prompts seed. A prompt is a reusable, server-side template for a conversation: the client lists them, the user picks one, and the server returns the message set. MCP Fusion treats them with the same engineering as tools: typed inputs, middleware, pagination and a loopback into your own tool pipeline.
Defining a prompt
import { z } from 'zod';
export default f.prompt('issue.review')
.describe('Review a pull request with the team checklist')
.input(z.object({
repo: z.string().describe('Repository slug'),
pr: z.number().describe('Pull request number'),
}))
.handler(async (ctx, args) => ({
description: 'Review acme/product#42 against the checklist',
messages: [
{ role: 'user', content: `Review PR ${args.pr} in ${args.repo}. Use the checklist.` },
{ role: 'assistant', content: 'Sharing the checklist first, then the diff.' },
],
}));The fluent form mirrors tools: .describe(), .input() (a Zod object, or a flat descriptor map of primitive arguments), .use(middleware), .title(), .icons(), .tags() and terminal .handler((ctx, args) => PromptResult). The result is { description?, messages } with user and assistant roles, multi-turn allowed, and content blocks beyond text: image, audio, resource_link, resource and the PromptMessage factory helpers (.system() is encoded as user: MCP has no system role).
The declarative form, definePrompt, and the f.presenter()-style f.prompt(name, config) exist for config-as-code layouts. Arguments compile to Zod with strict flat validation: only primitives, no arrays or nested objects. That is MCP's constraint, enforced at definition time with a clear error rather than at wire time.
Loopback: prompts that call tools
The prompt handler's context includes invokeTool(name, args), a dispatcher into the same tool pipeline with middleware, validation and Presenters. A prompt is therefore not a static string: it can fetch the live PR diff with the user's own tools, hydrate the checklist, and only then return the seeded messages. RBAC is enforced inside the loopback (the caller's context is the caller's context), and the request's AbortSignal propagates, so a cancelled prompt cancels the tool work it triggered.
Hydration is bounded: a per-registry deadline (.timeout(ms) on the builder or setDefaultHydrationTimeout) caps how long a prompt may spend calling tools before returning. Past the deadline the response ships with a hydration alert block instead of hanging the client.
Pagination and lifecycle
prompts/list is paginated server-side: CursorCodec turns the "after" name into a cursor that is either HMAC-SHA256 signed or AES-GCM encrypted (configured with a 32-byte secret; without one, a per-process ephemeral key). Page size defaults to 50, filtering by tags supported. A cursor is opaque to clients by design.
notifyPromptListChanged() (or the list_changed event path on f.prompt mutations at runtime) sends notifications/prompts/list_changed, debounced at 100ms, so a registry that grows during a session does not spam the client.
Interceptors
registry.useInterceptor(fn) runs after a handler returns and before the client sees the result: append a <compliance_notice>, prepend a user turn, or inject context blocks wrapped in a tag. Intercepting is the safe way to add organization-wide guidance without editing every prompt.
Why this matters
The integration tax for teams building on MCP is usually "the client only has tools". Prompts close the other half: you ship the workflow, not just the capability. Combined with Tools, Resources and the Wire format, a single connector can hand an agent the whole shape of how to work with your data, in the agent's native protocol.
Next steps
- Tools: the execution half
- Runtime architecture: where the registry sits
- Skills: prompts are also how SKILL.md ships to agents
