Adding MCP Prompts
Learn how to create custom MCP (Model Context Protocol) prompts to provide reusable prompt templates for Claude.
Overview
MCP prompts are predefined message templates that Claude can use to structure conversations. Unlike tools (which perform actions) and resources (which provide data), prompts are templates that help Claude initiate or structure interactions with users.
Use cases:
- Onboarding flows and welcome messages
- Role-specific system prompts
- User context personalization
- Consistent conversation starters
- Template-based interactions
Prompt Architecture
Prompt Components
src/mcp/prompts/
├── index.ts # Prompt registry
├── greeting.ts # Example: Basic prompt
├── user-context.ts # Example: Context-aware prompt
├── greeting.test.ts # Example prompt tests
└── user-context.test.tsPrompt workflow:
- Client calls
prompts/list→ Server returns available prompts - Client calls
prompts/getwith prompt name and arguments - Server generates prompt messages from template
- Client uses messages to initiate conversation
Prompt Structure
export function registerMyPrompt(server: McpServer): void {
server.registerPrompt(
"prompt_name", // Prompt identifier
{
description: "...", // Human-readable description
argsSchema: {...} // Zod schema for arguments
},
(args, extra) => { // Prompt handler function
// Generate messages from template
return {
messages: [
{
role: "assistant", // Message appears as assistant output in context
content: {
type: "text",
text: "..."
}
}
]
};
}
);
}Note: MCP prompts provide conversation context, not direct output. The returned messages are injected into the conversation history, and Claude then responds based on that context.
Creating a Prompt
Step 1: Create Prompt File
Create src/mcp/prompts/my_prompt.ts:
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { logger } from "../../services/logger.js";
/**
* My Custom Prompt
*
* Describe what this prompt does and when to use it.
*/
const myPromptArgsSchema = {
// Define prompt arguments using Zod
userName: z.string().optional().describe("User's name for personalization"),
context: z.string().optional().describe("Additional context"),
};
export function registerMyPrompt(server: McpServer): void {
server.registerPrompt(
"my-prompt",
{
description: "Description of what the prompt generates",
argsSchema: myPromptArgsSchema,
},
(args, extra) => {
try {
// Extract arguments
const userName = args.userName ?? "User";
const context = args.context ?? "";
logger.info("My prompt invoked", {
hasName: !!args.userName,
category: "mcp-prompt",
});
// Generate prompt text
const promptText = `Hello ${userName}! ${context}`;
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: promptText,
},
},
],
};
} catch (error) {
logger.error("My prompt error", {
error: error instanceof Error ? error.message : String(error),
category: "mcp-prompt",
});
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: `Error generating prompt: ${error instanceof Error ? error.message : "Unknown error"}`,
},
},
],
};
}
}
);
}Step 2: Define Arguments Schema
Use Zod to define and validate arguments:
const myPromptArgsSchema = {
// Required string
topic: z.string().describe("The topic to discuss"),
// Optional string with default
tone: z
.enum(["formal", "casual", "technical"])
.optional()
.describe("Conversation tone (default: casual)"),
// Optional number with constraints
detailLevel: z
.number()
.min(1)
.max(5)
.optional()
.describe("Level of detail from 1-5 (default: 3)"),
// Optional boolean
includeExamples: z
.boolean()
.optional()
.describe("Whether to include examples (default: true)"),
// Optional array
tags: z
.array(z.string())
.optional()
.describe("Relevant tags for context"),
};Step 3: Implement Prompt Logic
(args, extra) => {
try {
// 1. Extract and validate arguments
const topic = args.topic;
const tone = args.tone ?? "casual";
const detailLevel = args.detailLevel ?? 3;
const includeExamples = args.includeExamples ?? true;
// 2. Access user context if needed (see User Context section)
const userContext = extra?._meta?.user as UserContext | undefined;
// 3. Build prompt text based on arguments
let promptText = `Let's discuss ${topic}`;
if (tone === "formal") {
promptText = `I would like to formally discuss the topic of ${topic}`;
}
if (detailLevel > 3) {
promptText += " in depth";
}
if (includeExamples) {
promptText += ", with practical examples";
}
// 4. Log for observability
logger.info("Prompt generated", {
topic,
tone,
detailLevel,
category: "mcp-prompt",
});
// 5. Return message array
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: promptText,
},
},
],
};
} catch (error) {
// Always handle errors gracefully
logger.error("Prompt generation failed", {
error: error instanceof Error ? error.message : String(error),
category: "mcp-prompt",
});
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
},
],
};
}
}Step 4: Register Prompt
Add to src/mcp/prompts/index.ts:
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerGreetingPrompt } from "./greeting.js";
import { registerUserContextPrompt } from "./user-context.js";
import { registerMyPrompt } from "./my_prompt.js"; // Add import
export function registerPrompts(server: McpServer): void {
registerGreetingPrompt(server);
registerUserContextPrompt(server);
registerMyPrompt(server); // Add registration
}Step 5: Test the Prompt
Create src/mcp/prompts/my_prompt.test.ts:
import { describe, it, expect, beforeEach, vi } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerMyPrompt } from "./my_prompt.js";
import type { UserContext } from "../../types/index.js";
describe("My Prompt", () => {
let server: McpServer;
beforeEach(() => {
server = new McpServer({
name: "test-server",
version: "1.0.0",
});
registerMyPrompt(server);
});
it("should register prompt with correct name", () => {
const prompts = Array.from(server.getPrompts());
expect(prompts).toContainEqual(
expect.objectContaining({
name: "my-prompt",
})
);
});
it("should generate prompt with default arguments", async () => {
const handler = server.getPromptHandler("my-prompt");
expect(handler).toBeDefined();
const result = await handler!(
{ topic: "TypeScript" },
{ _meta: {} }
);
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe("user");
expect(result.messages[0].content.text).toContain("TypeScript");
});
it("should handle custom arguments", async () => {
const handler = server.getPromptHandler("my-prompt");
const result = await handler!(
{
topic: "Testing",
tone: "formal",
detailLevel: 5,
includeExamples: true,
},
{ _meta: {} }
);
expect(result.messages[0].content.text).toContain("Testing");
expect(result.messages[0].content.text).toContain("examples");
});
it("should handle errors gracefully", async () => {
const handler = server.getPromptHandler("my-prompt");
// Pass invalid arguments
const result = await handler!(
{ topic: null } as any,
{ _meta: {} }
);
expect(result.messages[0].content.text).toContain("Error");
});
});Accessing User Context
Prompts can access authenticated user information from JWT claims:
import type { UserContext } from "../../types/index.js";
export function registerPersonalizedPrompt(server: McpServer): void {
server.registerPrompt(
"personalized",
{
description: "Generate personalized prompt using user context",
argsSchema: {},
},
(args, extra) => {
// Extract user context from metadata
const userContext = extra?._meta?.user as UserContext | undefined;
if (!userContext) {
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: "Error: User context not available",
},
},
],
};
}
// Use user information
const userName = userContext.name ?? userContext.email ?? "User";
const userId = userContext.sub;
const userGroups = userContext.groups ?? [];
const promptText = `Welcome back, ${userName}!
Your ID: ${userId}
Your groups: ${userGroups.join(", ") || "none"}
How can I assist you today?`;
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: promptText,
},
},
],
};
}
);
}Available user context fields:
sub- User ID (subject claim)email- User email addressname- User display namegroups- User group memberships (array)
When user context is unavailable:
AUTH_REQUIRED=false- No user context available- Invalid or missing JWT - User context will be undefined
Multi-Message Prompts
Prompts can return multiple messages to set up context:
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: "I'm ready to help with your task.",
},
},
{
role: "user",
content: {
type: "text",
text: "Please analyze this data set...",
},
},
],
};Message roles:
"assistant"- Message appears as prior assistant output in conversation context"user"- Message appears as prior user input in conversation context
Use cases:
- Assistant greeting + follow-up context
- Multi-turn conversation starters
- Role-playing scenarios
- Setting up conversation flow
Advanced Patterns
Dynamic Content Generation
Generate prompt content based on complex logic:
export function registerAnalysisPrompt(server: McpServer): void {
server.registerPrompt(
"analysis-starter",
{
description: "Generate analysis prompt based on data type",
argsSchema: {
dataType: z.enum(["time-series", "categorical", "text", "image"]),
goals: z.array(z.string()).optional(),
},
},
(args) => {
const templates = {
"time-series": "Analyze temporal patterns, trends, and seasonality in",
categorical: "Examine distribution, frequencies, and relationships in",
text: "Perform sentiment analysis and topic modeling on",
image: "Detect objects, patterns, and visual features in",
};
const analysis = templates[args.dataType];
const goals = args.goals?.join(", ") ?? "general insights";
const promptText = `${analysis} the provided data.
Focus areas: ${goals}
Please provide:
1. Key observations
2. Statistical summary
3. Actionable recommendations`;
return {
messages: [
{
role: "assistant",
content: { type: "text", text: promptText },
},
],
};
}
);
}Conditional Prompts
Adapt prompts based on user role or context:
export function registerRoleBasedPrompt(server: McpServer): void {
server.registerPrompt(
"role-based",
{
description: "Generate role-specific prompts",
argsSchema: {},
},
(args, extra) => {
const userContext = extra?._meta?.user as UserContext | undefined;
const groups = userContext?.groups ?? [];
let promptText = "Welcome! ";
if (groups.includes("admin")) {
promptText += "As an admin, you have full access to system management.";
} else if (groups.includes("developer")) {
promptText += "As a developer, you can access development tools and APIs.";
} else {
promptText += "You have standard user access.";
}
return {
messages: [
{
role: "assistant",
content: { type: "text", text: promptText },
},
],
};
}
);
}Template Composition
Build complex prompts from reusable components:
const buildIntroduction = (name: string) =>
`Hello ${name}! I'm here to assist you.`;
const buildInstructions = (task: string) =>
`To complete "${task}", please follow these steps:`;
const buildConclusion = () =>
`Let me know if you need any clarification!`;
export function registerComposedPrompt(server: McpServer): void {
server.registerPrompt(
"composed",
{
description: "Composed prompt from multiple templates",
argsSchema: {
name: z.string(),
task: z.string(),
steps: z.array(z.string()),
},
},
(args) => {
const intro = buildIntroduction(args.name);
const instructions = buildInstructions(args.task);
const stepsList = args.steps.map((s, i) => `${i + 1}. ${s}`).join("\n");
const conclusion = buildConclusion();
const promptText = `${intro}
${instructions}
${stepsList}
${conclusion}`;
return {
messages: [
{
role: "assistant",
content: { type: "text", text: promptText },
},
],
};
}
);
}Best Practices
Naming Conventions
Prompt names:
- Use kebab-case:
user-onboarding,code-review-start - Be descriptive:
technical-docs-templatenot justtemplate - Include category for organization:
support-ticket-triage,support-escalation
File names:
- Match prompt name:
user-onboarding.tsforuser-onboardingprompt - Include tests:
user-onboarding.test.ts - Group related prompts in subdirectories if needed
Description Guidelines
Write clear, actionable descriptions:
Good:
description: "Generate a welcome message for new users with onboarding steps and helpful resources"Bad:
description: "Welcome prompt" // Too vagueInclude:
- What the prompt generates
- When to use it
- What arguments affect the output
Argument Schema Design
Make arguments intuitive:
// Good: Clear argument names and descriptions
const schema = {
userLevel: z.enum(["beginner", "intermediate", "expert"])
.describe("User's expertise level for appropriate content"),
includeCode: z.boolean()
.optional()
.describe("Whether to include code examples (default: true)"),
};
// Bad: Unclear names
const schema = {
level: z.number(), // What scale? What does the number mean?
flag: z.boolean(), // Which flag? What does it control?
};Provide sensible defaults:
const tone = args.tone ?? "friendly";
const verbosity = args.verbosity ?? 3; // Medium detail
const includeExamples = args.includeExamples ?? true;Error Handling
Always handle errors gracefully:
(args, extra) => {
try {
// Validate critical arguments
if (!args.requiredField) {
throw new Error("requiredField is required");
}
// Generate prompt
const promptText = generatePrompt(args);
return {
messages: [
{
role: "assistant",
content: { type: "text", text: promptText },
},
],
};
} catch (error) {
// Log error for debugging
logger.error("Prompt generation failed", {
error: error instanceof Error ? error.message : String(error),
promptName: "my-prompt",
category: "mcp-prompt",
});
// Return user-friendly error message
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: `Unable to generate prompt: ${error instanceof Error ? error.message : "Unknown error"}`,
},
},
],
};
}
}Logging
Use structured logging for observability:
// Log prompt invocation
logger.info("Prompt invoked", {
promptName: "my-prompt",
argumentCount: Object.keys(args).length,
hasUserContext: !!extra?._meta?.user,
category: "mcp-prompt",
});
// Log generation details
logger.debug("Prompt generated", {
promptName: "my-prompt",
messageCount: messages.length,
totalLength: messages.reduce((sum, m) => sum + m.content.text.length, 0),
category: "mcp-prompt",
});
// Log errors with context
logger.error("Prompt error", {
promptName: "my-prompt",
error: error.message,
args: JSON.stringify(args),
category: "mcp-prompt",
});Testing
Test all aspects of prompt behavior:
describe("My Prompt", () => {
// Test registration
it("should register with correct metadata", () => {
const prompts = Array.from(server.getPrompts());
expect(prompts).toContainEqual(
expect.objectContaining({
name: "my-prompt",
description: expect.stringContaining("..."),
})
);
});
// Test default behavior
it("should generate prompt with defaults", async () => {
const result = await handler!({}, { _meta: {} });
expect(result.messages).toHaveLength(1);
expect(result.messages[0].content.text).toBeTruthy();
});
// Test argument handling
it("should handle all argument combinations", async () => {
const testCases = [
{ args: { tone: "formal" }, expected: "formal language" },
{ args: { tone: "casual" }, expected: "casual language" },
];
for (const testCase of testCases) {
const result = await handler!(testCase.args, { _meta: {} });
expect(result.messages[0].content.text).toContain(testCase.expected);
}
});
// Test user context
it("should use user context when available", async () => {
const mockUser: UserContext = {
sub: "user-123",
email: "user@example.com",
name: "Test User",
groups: ["developers"],
};
const result = await handler!({}, { _meta: { user: mockUser } });
expect(result.messages[0].content.text).toContain("Test User");
});
// Test error handling
it("should handle errors gracefully", async () => {
const result = await handler!({ invalid: "data" } as any, { _meta: {} });
expect(result.messages[0].content.text).toContain("Error");
});
});Common Patterns
Onboarding Prompts
Guide new users through initial steps:
export function registerOnboardingPrompt(server: McpServer): void {
server.registerPrompt(
"user-onboarding",
{
description: "Generate step-by-step onboarding for new users",
argsSchema: {
userName: z.string().optional(),
},
},
(args) => {
const name = args.userName ?? "there";
const promptText = `Welcome ${name}!
Let's get you started:
1. **Explore Features**: Browse available tools and prompts
2. **Try Examples**: Test with sample data
3. **Read Docs**: Check out guides and references
4. **Get Help**: Use the /help command anytime
What would you like to do first?`;
return {
messages: [
{
role: "assistant",
content: { type: "text", text: promptText },
},
],
};
}
);
}Task Templates
Provide structure for common tasks:
export function registerTaskPrompt(server: McpServer): void {
server.registerPrompt(
"task-template",
{
description: "Generate structured task prompt",
argsSchema: {
taskType: z.enum(["bug-fix", "feature", "refactor", "docs"]),
context: z.string().optional(),
},
},
(args) => {
const templates = {
"bug-fix": `## Bug Fix Task
**Context:** ${args.context ?? "N/A"}
Please help with:
1. Reproduce the issue
2. Identify root cause
3. Implement fix
4. Add test coverage
5. Verify resolution`,
feature: `## Feature Implementation
**Context:** ${args.context ?? "N/A"}
Let's implement this feature:
1. Design the solution
2. Write the code
3. Add tests
4. Update documentation
5. Review changes`,
};
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: templates[args.taskType] ?? "Task template",
},
},
],
};
}
);
}Troubleshooting
Prompt Not Listed
Issue: Prompt doesn't appear in prompts/list
Solutions:
Check registration in
src/mcp/prompts/index.ts:typescriptregisterMyPrompt(server); // Must be calledVerify prompt name is unique (no duplicates)
Check server logs for registration errors
Arguments Not Validated
Issue: Invalid arguments not rejected
Solution: Ensure Zod schema is properly defined:
// Bad: No validation
argsSchema: {}
// Good: Proper validation
argsSchema: {
name: z.string().min(1).describe("Required name field"),
age: z.number().positive().describe("Must be positive number"),
}User Context Undefined
Issue: extra?._meta?.user is always undefined
Possible causes:
AUTH_REQUIRED=false- User context disabled- Missing or invalid JWT token
- Token claims don't include expected fields
Solution:
const userContext = extra?._meta?.user as UserContext | undefined;
if (!userContext) {
logger.warn("User context unavailable", {
authRequired: process.env.AUTH_REQUIRED,
category: "mcp-prompt",
});
// Return generic prompt or error message
return {
messages: [
{
role: "assistant",
content: {
type: "text",
text: "User context not available",
},
},
],
};
}Related Documentation
- MCP Tools - Creating executable MCP tools
- MCP Resources - Providing data through MCP resources
- Testing Guide - Testing strategies and patterns
- Code Quality - Code standards and linting