Introduction

Knowing the API for registering prompts is only half the picture. The other half is deciding when to use a prompt versus a tool, how to compose prompts for complex workflows, and how to test prompts effectively. Good prompt design makes your MCP server intuitive and powerful for end users.

Key Concepts

  • Prompts vs tools: prompts prepare the LLM with context and instructions; tools perform actions and return results
  • Workflow starters: prompts are ideal as entry points that set up a multi-step interaction
  • Prompt composition: break complex workflows into focused prompts that can be combined
  • Local testing: test prompt handlers by calling them directly with sample arguments

Real World Context

Consider a development assistant MCP server. Here is how you would divide responsibilities:

TaskPrimitiveWhy
Review a pull requestPromptSets up the review context and instructions for the LLM
Fetch PR diff from GitHubToolPerforms an API call and returns data
Read the project's style guideResourceExposes static reference data
Generate test casesPromptStructures the LLM's approach to test generation
Run the test suiteToolExecutes a command and returns results

Prompts and tools often work together: a prompt sets up the context, and the LLM uses tools to gather information and take action.

Deep Dive

The key question is: "Am I giving the LLM instructions, or am I giving it a capability?" Prompts are instructions; tools are capabilities.

typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

// This is a PROMPT — it structures how the LLM should think
server.registerPrompt(
  'analyze-performance',
  {
    title: 'Analyze Performance',
    description: 'Systematic performance analysis of a code module',
    argsSchema: z.object({
      moduleName: z.string().describe('Name of the module to analyze')
    })
  },
  ({ moduleName }): GetPromptResult => ({
    messages: [{
      role: 'user',
      content: {
        type: 'text',
        text: [
          `Perform a systematic performance analysis of the ${moduleName} module.`,
          '',
          'Follow these steps:',
          '1. Read the module source code using the read-file tool',
          '2. Identify hot paths and frequently called functions',
          '3. Check for common performance anti-patterns:',
          '   - Unnecessary allocations in loops',
          '   - Missing memoization for expensive computations',
          '   - Synchronous I/O in async contexts',
          '   - N+1 query patterns',
          '4. Suggest specific improvements with code examples',
          '5. Estimate the impact of each suggestion (high/medium/low)'
        ].join('\n')
      }
    }]
  })
);

This prompt references tools ("use the read-file tool") but does not implement them — it guides the LLM's workflow while letting tools handle the actual data access.

Workflow starters are prompts designed to kick off a multi-step process. They set the context and let the LLM drive the subsequent tool calls.

typescript
server.registerPrompt(
  'debug-issue',
  {
    title: 'Debug Issue',
    description: 'Systematic debugging workflow for a reported issue',
    argsSchema: z.object({
      issueDescription: z.string().describe('Description of the bug or issue'),
      reproSteps: z.string().optional().describe('Steps to reproduce')
    })
  },
  ({ issueDescription, reproSteps }): GetPromptResult => {
    const messages: GetPromptResult['messages'] = [
      {
        role: 'user',
        content: {
          type: 'text',
          text: [
            `Debug this issue: ${issueDescription}`,
            '',
            reproSteps ? `Reproduction steps: ${reproSteps}` : 'No reproduction steps provided.',
            '',
            'Approach:',
            '1. Identify the likely source files involved',
            '2. Read the relevant code',
            '3. Form a hypothesis about the root cause',
            '4. Suggest a fix with before/after code'
          ].join('\n')
        }
      }
    ];
    return { messages };
  }
);

The prompt structures the debugging approach but relies on the LLM to use tools for reading files and exploring the codebase.

Testing prompts locally is straightforward: call the handler directly and inspect the output.

typescript
// Test a prompt handler directly
const result = reviewHandler({ code: 'function add(a, b) { return a + b; }', language: 'typescript' });
console.log(JSON.stringify(result, null, 2));
// Verify:
// - messages array is not empty
// - roles are correct
// - text includes the provided arguments
// - instructions are clear and complete

Since prompt handlers are pure functions (input arguments in, messages out), they are easy to unit test without any server infrastructure.

Common Pitfalls

  • Using a tool when a prompt is more appropriate: if the LLM needs to think and reason rather than fetch data, use a prompt
  • Overloading a single prompt: one prompt should cover one workflow; split complex multi-phase processes into separate prompts
  • Not referencing available tools: if your server also exposes tools, mention them in the prompt so the LLM knows they are available
  • Skipping local testing: always test handlers with edge cases (empty strings, very long input, special characters) before deploying

Best Practices

  • Design prompts as workflow entry points that guide the LLM through a structured process
  • Reference your server's tools and resources in prompt text so the LLM knows what capabilities it has
  • Keep each prompt focused on a single workflow — compose complex processes by using multiple prompts sequentially
  • Write unit tests for prompt handlers by calling them directly with various argument combinations
  • Use numbered steps in prompt text to give the LLM a clear sequence to follow

Summary

Prompt design in MCP is about choosing the right primitive for each task. Prompts provide instructions and structure; tools provide capabilities. Well-designed prompts serve as workflow starters that guide the LLM through multi-step processes, referencing tools and resources as needed. Test prompt handlers locally as pure functions to validate output quality before deployment.

Code Examples

typescript
server.registerPrompt(
  'analyze-performance',
  {
    title: 'Analyze Performance',
    description: 'Systematic performance analysis of a code module',
    argsSchema: z.object({
      moduleName: z.string().describe('Name of the module to analyze')
    })
  },
  ({ moduleName }): GetPromptResult => ({
    messages: [{
      role: 'user',
      content: {
        type: 'text',
        text: `Analyze performance of ${moduleName}.\n\n1. Read the source code\n2. Identify hot paths\n3. Check for anti-patterns\n4. Suggest improvements`
      }
    }]
  })
);
typescript
// Call the handler directly to verify output
const result = reviewHandler({
  code: 'function add(a, b) { return a + b; }',
  language: 'typescript'
});
console.log(JSON.stringify(result, null, 2));
// Check: messages array, roles, argument interpolation
✓ Completed