Introduction
MCP prompts go beyond simple templates. They enable specialized workflows where prompts act as entry points for complex tasks like code review, data analysis, or multi-step operations. This lesson covers using prompts for specialized tasks, creating dynamic prompts that adapt based on arguments, chaining prompts together, and exposing prompts as UI affordances such as slash commands.
Key Concepts
- Specialized Prompts: Prompts designed for specific tasks (code review, data analysis, summarization) that encode domain expertise into reusable templates.
- Dynamic Prompts: Prompts whose output changes significantly based on the arguments provided, adapting their instructions, examples, or context.
- Prompt Chaining: Using the output of one prompt as input to another, building multi-step workflows from composable prompt primitives.
- Prompt as UI Affordance: Exposing discovered prompts as slash commands, menu items, or buttons in the user interface.
Real World Context
A development team uses an MCP server that exposes three prompts: code-review, refactor-suggestion, and test-generation. Their IDE extension discovers these prompts at startup and presents them as slash commands. A developer types /code-review in the chat, the extension retrieves the prompt with the current file content as an argument, and the LLM receives a carefully crafted review prompt. If the review identifies issues, the developer chains into /refactor-suggestion with the LLM's findings.
Deep Dive
Specialized prompts encode expert knowledge. Instead of writing generic instructions, a server exposes prompts tailored for specific tasks.
The following code shows how a client uses a specialized prompt for different tasks:
typescript// Discover available specialized prompts const { prompts } = await client.listPrompts(); // e.g. ["code-review", "data-analysis", "incident-report"] async function executeSpecializedTask(taskName: string, args: Record<string, string>) { const prompt = prompts.find(p => p.name === taskName); if (!prompt) { throw new Error(`Unknown task: ${taskName}`); } const result = await client.getPrompt({ name: taskName, arguments: args }); const messages = result.messages.map(m => ({ role: m.role, content: m.content.type === 'text' ? m.content.text : '' })); return await llm.chat(messages); } // Use different specialized prompts const review = await executeSpecializedTask('code-review', { language: 'typescript', code: sourceCode }); const analysis = await executeSpecializedTask('data-analysis', { format: 'csv', query: 'Find sales trends' });
This pattern treats each prompt as a named capability that the client can invoke with appropriate arguments.
Dynamic prompts adapt their output based on arguments. The same prompt name can produce very different messages depending on the inputs.
Here is an example of leveraging a dynamic prompt that adapts based on severity level:
typescript// The same prompt produces different outputs based on arguments const quickReview = await client.getPrompt({ name: 'code-review', arguments: { depth: 'quick', focus: 'bugs' } }); // Returns a concise, bug-focused review prompt const deepReview = await client.getPrompt({ name: 'code-review', arguments: { depth: 'thorough', focus: 'architecture' } }); // Returns a detailed, architecture-focused review prompt
The server handles the logic of adapting the prompt content. Your client simply passes the appropriate arguments.
Prompt chaining combines multiple prompts into a workflow. The output of one prompt-driven interaction feeds into the next.
The following code demonstrates a two-step prompt chain:
typescriptasync function chainedWorkflow(code: string) { // Step 1: Review the code const reviewPrompt = await client.getPrompt({ name: 'code-review', arguments: { code, language: 'typescript' } }); const reviewMessages = reviewPrompt.messages.map(m => ({ role: m.role, content: m.content.type === 'text' ? m.content.text : '' })); const reviewResult = await llm.chat(reviewMessages); // Step 2: Generate fixes based on the review const fixPrompt = await client.getPrompt({ name: 'generate-fixes', arguments: { code, issues: reviewResult.content } }); const fixMessages = fixPrompt.messages.map(m => ({ role: m.role, content: m.content.type === 'text' ? m.content.text : '' })); const fixResult = await llm.chat(fixMessages); return { review: reviewResult, fixes: fixResult }; }
Each step retrieves a fresh prompt, passes relevant context from the previous step, and feeds the result to the LLM.
Exposing prompts as UI affordances makes them discoverable to users. The most common pattern is slash commands.
Here is how to build a slash command system from MCP prompts:
typescriptclass PromptCommandSystem { private commands = new Map<string, any>(); async initialize(client: Client) { const { prompts } = await client.listPrompts(); for (const prompt of prompts) { this.commands.set(`/${prompt.name}`, prompt); } } getAvailableCommands(): string[] { return Array.from(this.commands.keys()); } async execute(command: string, args: Record<string, string>, client: Client) { const prompt = this.commands.get(command); if (!prompt) return null; const result = await client.getPrompt({ name: prompt.name, arguments: args }); return result.messages; } }
This maps each prompt name to a slash command, building the UI from server-provided metadata.
Common Pitfalls
- Tight coupling between chained prompts: If one prompt's output format changes, the next prompt in the chain may break. Use stable argument contracts between steps.
- Overloading a single prompt: Trying to make one prompt handle every scenario leads to bloated, unfocused templates. Prefer multiple specialized prompts over one generic one.
- Not handling missing prompts gracefully: Servers may add or remove prompts between sessions. Your slash command system should handle missing prompts without crashing.
Best Practices
- Map MCP prompts to UI elements (slash commands, menus) at startup and refresh periodically so users always see current capabilities.
- When chaining prompts, pass only the essential output from each step to keep context focused and avoid exceeding token limits.
- Log the full prompt chain for debugging, including which arguments were passed at each step and what the LLM returned.
Summary
- Specialized prompts encode domain expertise for tasks like code review and data analysis.
- Dynamic prompts adapt their output based on argument values, enabling flexible behavior from a single prompt definition.
- Prompt chaining creates multi-step workflows by feeding the output of one prompt-driven interaction into the next.
- Exposing prompts as slash commands or UI affordances makes server capabilities discoverable and intuitive for users.
- Keep prompt chains loosely coupled and handle missing prompts gracefully.
Code Examples
async function chainedWorkflow(code: string) {
const reviewPrompt = await client.getPrompt({
name: 'code-review',
arguments: { code, language: 'typescript' }
});
const reviewMessages = reviewPrompt.messages.map(m => ({
role: m.role,
content: m.content.type === 'text' ? m.content.text : ''
}));
const reviewResult = await llm.chat(reviewMessages);
const fixPrompt = await client.getPrompt({
name: 'generate-fixes',
arguments: { code, issues: reviewResult.content }
});
const fixMessages = fixPrompt.messages.map(m => ({
role: m.role,
content: m.content.type === 'text' ? m.content.text : ''
}));
return await llm.chat(fixMessages);
}class PromptCommandSystem {
private commands = new Map<string, any>();
async initialize(client: Client) {
const { prompts } = await client.listPrompts();
for (const prompt of prompts) {
this.commands.set(`/${prompt.name}`, prompt);
}
}
getAvailableCommands(): string[] {
return Array.from(this.commands.keys());
}
async execute(command: string, args: Record<string, string>, client: Client) {
const prompt = this.commands.get(command);
if (!prompt) return null;
return (await client.getPrompt({ name: prompt.name, arguments: args })).messages;
}
}