Mcp

MCP Prompt Templates and Dynamic Content

Complete guide to MCP prompt templates and dynamic content, covering prompt resource design, template variables, context injection, multi-step prompt chains, dynamic prompt generation from data sources, and building reusable prompt libraries.

MCP Prompt Templates and Dynamic Content

Overview

MCP prompts are pre-built instructions that the server offers to the client. They are not just static text — they can accept arguments, pull in dynamic data, chain together, and adapt to context. Think of them as reusable, parameterized prompt recipes that your MCP server publishes. I use MCP prompts to encapsulate complex workflows that would otherwise require the user to write the same detailed instructions every time — code review templates, data analysis workflows, migration plan generators. The prompt does the thinking about what to ask; the model does the thinking about the answer.

Prerequisites

  • Node.js 16 or later
  • @modelcontextprotocol/sdk package installed
  • Understanding of MCP server basics (tools, resources, prompts)
  • Familiarity with prompt engineering concepts
  • A use case where reusable, parameterized prompts would save time

Understanding MCP Prompts

MCP prompts have three key properties: they have a name, they accept arguments, and they return messages that the client can send to the model.

// A prompt returns an array of messages
// These messages are injected into the conversation
{
  description: "Analyze a database table",
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: "Analyze the 'users' table. Here is the schema: ..."
      }
    }
  ]
}

Prompts vs Tools vs Resources

Resources: "Here is data you can read" (passive, read-only)
Tools:     "Here is an action you can take" (active, side effects)
Prompts:   "Here is a workflow you can start" (interactive, templates)

Basic Prompt Server

var Server = require("@modelcontextprotocol/sdk/server/index.js").Server;
var StdioTransport = require("@modelcontextprotocol/sdk/server/stdio.js").StdioServerTransport;

var server = new Server(
  { name: "prompt-templates", version: "1.0.0" },
  { capabilities: { prompts: {} } }
);

// List available prompts
server.setRequestHandler("prompts/list", function() {
  return {
    prompts: [
      {
        name: "code-review",
        description: "Review code for bugs, security issues, performance, and best practices",
        arguments: [
          {
            name: "code",
            description: "The code to review",
            required: true
          },
          {
            name: "language",
            description: "Programming language (javascript, python, etc.)",
            required: false
          },
          {
            name: "focus",
            description: "Review focus: security, performance, readability, or all",
            required: false
          }
        ]
      },
      {
        name: "explain-error",
        description: "Explain an error message and suggest fixes",
        arguments: [
          {
            name: "error",
            description: "The error message or stack trace",
            required: true
          },
          {
            name: "context",
            description: "What you were doing when the error occurred",
            required: false
          }
        ]
      },
      {
        name: "write-tests",
        description: "Generate test cases for a function or module",
        arguments: [
          {
            name: "code",
            description: "The code to write tests for",
            required: true
          },
          {
            name: "framework",
            description: "Test framework: jest, mocha, or tap",
            required: false
          }
        ]
      }
    ]
  };
});

// Get a specific prompt with arguments filled in
server.setRequestHandler("prompts/get", function(request) {
  var name = request.params.name;
  var args = request.params.arguments || {};

  switch (name) {
    case "code-review":
      return getCodeReviewPrompt(args);
    case "explain-error":
      return getExplainErrorPrompt(args);
    case "write-tests":
      return getWriteTestsPrompt(args);
    default:
      throw new Error("Unknown prompt: " + name);
  }
});

function getCodeReviewPrompt(args) {
  var language = args.language || "javascript";
  var focus = args.focus || "all";

  var focusInstructions = {
    security: "Focus exclusively on security vulnerabilities: injection attacks, authentication flaws, data exposure, insecure dependencies, and OWASP Top 10 issues.",
    performance: "Focus exclusively on performance: algorithmic complexity, memory leaks, unnecessary allocations, N+1 queries, missing caching, and blocking operations.",
    readability: "Focus exclusively on readability: naming conventions, function length, code organization, comments quality, and maintainability.",
    all: "Review for security vulnerabilities, performance issues, readability, and adherence to best practices."
  };

  var reviewFocus = focusInstructions[focus] || focusInstructions.all;

  return {
    description: "Code review for " + language + " (" + focus + " focus)",
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: "Please review the following " + language + " code.\n\n"
            + reviewFocus + "\n\n"
            + "For each issue found, provide:\n"
            + "1. The specific line or section\n"
            + "2. What the issue is\n"
            + "3. Why it matters\n"
            + "4. How to fix it with a code example\n\n"
            + "Rate the overall code quality from 1-10.\n\n"
            + "```" + language + "\n"
            + args.code + "\n"
            + "```"
        }
      }
    ]
  };
}

function getExplainErrorPrompt(args) {
  var contextLine = args.context
    ? "\n\nContext: " + args.context
    : "";

  return {
    description: "Error explanation and fix suggestions",
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: "I encountered the following error:" + contextLine + "\n\n"
            + "```\n" + args.error + "\n```\n\n"
            + "Please:\n"
            + "1. Explain what this error means in plain language\n"
            + "2. Identify the most likely cause\n"
            + "3. Provide 2-3 possible fixes, starting with the most likely\n"
            + "4. Show code examples for each fix\n"
            + "5. Explain how to prevent this error in the future"
        }
      }
    ]
  };
}

function getWriteTestsPrompt(args) {
  var framework = args.framework || "jest";

  return {
    description: "Test generation using " + framework,
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: "Write comprehensive tests for the following code using " + framework + ".\n\n"
            + "```javascript\n" + args.code + "\n```\n\n"
            + "Include:\n"
            + "- Happy path tests for all public functions\n"
            + "- Edge cases (null, undefined, empty, boundary values)\n"
            + "- Error handling tests\n"
            + "- Any mocking needed for external dependencies\n\n"
            + "Use descriptive test names that explain the expected behavior.\n"
            + "Use `var` and `function()` syntax (not const/let/arrow)."
        }
      }
    ]
  };
}

var transport = new StdioTransport();
server.connect(transport).then(function() {
  console.error("Prompt templates MCP server running");
});

Dynamic Content Injection

Prompts become powerful when they pull in live data from your project.

File-Aware Prompts

var fs = require("fs");
var path = require("path");

var PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();

// Prompt that pulls in actual project files
function getRefactorPrompt(args) {
  var filePath = path.join(PROJECT_DIR, args.file);

  // Validate path is within project
  if (!path.resolve(filePath).startsWith(path.resolve(PROJECT_DIR))) {
    throw new Error("File must be within the project directory");
  }

  if (!fs.existsSync(filePath)) {
    throw new Error("File not found: " + args.file);
  }

  var fileContent = fs.readFileSync(filePath, "utf8");
  var ext = path.extname(filePath).slice(1);

  // Also read related files if they exist
  var relatedFiles = [];
  var testFile = filePath.replace(".js", ".test.js");
  if (fs.existsSync(testFile)) {
    relatedFiles.push({
      path: path.relative(PROJECT_DIR, testFile),
      content: fs.readFileSync(testFile, "utf8")
    });
  }

  var messages = [
    {
      role: "user",
      content: {
        type: "text",
        text: "Refactor the following file: `" + args.file + "`\n\n"
          + (args.goal ? "Refactoring goal: " + args.goal + "\n\n" : "")
          + "Current implementation:\n"
          + "```" + ext + "\n" + fileContent + "\n```"
      }
    }
  ];

  if (relatedFiles.length > 0) {
    var relatedText = relatedFiles.map(function(rf) {
      return "Related file `" + rf.path + "`:\n```" + ext + "\n" + rf.content + "\n```";
    }).join("\n\n");

    messages.push({
      role: "user",
      content: {
        type: "text",
        text: relatedText + "\n\nPlease update the test file to match any refactored interfaces."
      }
    });
  }

  return {
    description: "Refactor " + args.file,
    messages: messages
  };
}

Database-Aware Prompts

var pg = require("pg");
var pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 3 });

// Prompt that includes live database schema
function getDataAnalysisPrompt(args) {
  var tableName = args.table;

  if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
    throw new Error("Invalid table name");
  }

  return Promise.all([
    // Get schema
    pool.query(
      "SELECT column_name, data_type, is_nullable " +
      "FROM information_schema.columns " +
      "WHERE table_schema = 'public' AND table_name = $1 " +
      "ORDER BY ordinal_position",
      [tableName]
    ),
    // Get sample data
    pool.query("SELECT * FROM public." + tableName + " LIMIT 5"),
    // Get row count
    pool.query("SELECT COUNT(*) as count FROM public." + tableName)
  ]).then(function(results) {
    var schema = results[0].rows;
    var samples = results[1].rows;
    var count = results[2].rows[0].count;

    var schemaText = schema.map(function(col) {
      return "  " + col.column_name + " " + col.data_type + (col.is_nullable === "NO" ? " NOT NULL" : "");
    }).join("\n");

    return {
      description: "Data analysis for " + tableName + " (" + count + " rows)",
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: "Analyze the `" + tableName + "` table (" + count + " total rows).\n\n"
              + "Schema:\n```\n" + schemaText + "\n```\n\n"
              + "Sample data:\n```json\n" + JSON.stringify(samples, null, 2) + "\n```\n\n"
              + (args.question
                ? "Specific question: " + args.question + "\n\n"
                : "")
              + "Provide:\n"
              + "1. Summary of what this table contains\n"
              + "2. Data quality observations from the sample\n"
              + "3. Suggested SQL queries for common analyses\n"
              + "4. Any potential issues or optimizations"
          }
        }
      ]
    };
  });
}

Template Variable System

For complex prompts with many variables, build a template engine:

// Simple template engine for prompt text
function renderTemplate(template, variables) {
  return template.replace(/\{\{(\w+)(?:\|([^}]+))?\}\}/g, function(match, name, defaultValue) {
    if (variables[name] !== undefined && variables[name] !== null && variables[name] !== "") {
      return variables[name];
    }
    if (defaultValue !== undefined) {
      return defaultValue;
    }
    return match; // Leave unreplaced if no value and no default
  });
}

// Template with conditionals
function renderConditional(template, variables) {
  // Handle {{#if var}}...{{/if}} blocks
  template = template.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, function(match, name, content) {
    if (variables[name]) {
      return renderTemplate(content, variables);
    }
    return "";
  });

  // Handle {{#each items}}...{{/each}} blocks
  template = template.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, function(match, name, content) {
    var items = variables[name];
    if (!Array.isArray(items)) return "";

    return items.map(function(item, index) {
      var itemVars = Object.assign({}, variables, { item: item, index: index });
      return renderTemplate(content, itemVars);
    }).join("");
  });

  return renderTemplate(template, variables);
}

// Usage in prompt definitions
var PROMPT_TEMPLATES = {
  "api-design": {
    template: "Design a REST API for {{resource}}.\n\n"
      + "Requirements:\n"
      + "- Base URL: {{baseUrl|/api/v1}}\n"
      + "- Authentication: {{auth|Bearer token}}\n"
      + "{{#if existingEndpoints}}"
      + "\nExisting endpoints to be compatible with:\n{{existingEndpoints}}\n"
      + "{{/if}}"
      + "\nInclude:\n"
      + "1. All CRUD endpoints with request/response schemas\n"
      + "2. Pagination, filtering, and sorting\n"
      + "3. Error response format\n"
      + "4. Rate limiting headers\n"
      + "{{#if includeOpenApi}}5. OpenAPI 3.0 specification\n{{/if}}",
    arguments: [
      { name: "resource", required: true, description: "The resource to design an API for (e.g., 'users', 'products')" },
      { name: "baseUrl", required: false, description: "Base URL path" },
      { name: "auth", required: false, description: "Authentication method" },
      { name: "existingEndpoints", required: false, description: "Existing API endpoints to maintain compatibility with" },
      { name: "includeOpenApi", required: false, description: "Include OpenAPI spec in the output" }
    ]
  },

  "migration-plan": {
    template: "Create a database migration plan.\n\n"
      + "Current schema:\n```sql\n{{currentSchema}}\n```\n\n"
      + "Target changes:\n{{changes}}\n\n"
      + "Requirements:\n"
      + "- Zero-downtime migration\n"
      + "- Rollback plan for each step\n"
      + "- Data backfill strategy if needed\n"
      + "{{#if constraints}}\nConstraints: {{constraints}}\n{{/if}}"
      + "{{#if estimatedRows}}\nEstimated rows affected: {{estimatedRows}}\n{{/if}}",
    arguments: [
      { name: "currentSchema", required: true, description: "Current database schema (SQL)" },
      { name: "changes", required: true, description: "What needs to change" },
      { name: "constraints", required: false, description: "Any constraints (downtime window, etc.)" },
      { name: "estimatedRows", required: false, description: "Estimated rows in affected tables" }
    ]
  }
};

// Register all template-based prompts
server.setRequestHandler("prompts/list", function() {
  var prompts = Object.keys(PROMPT_TEMPLATES).map(function(name) {
    var tmpl = PROMPT_TEMPLATES[name];
    return {
      name: name,
      description: tmpl.template.split("\n")[0].replace(/\{\{[^}]+\}\}/g, "..."),
      arguments: tmpl.arguments
    };
  });
  return { prompts: prompts };
});

server.setRequestHandler("prompts/get", function(request) {
  var name = request.params.name;
  var args = request.params.arguments || {};
  var tmpl = PROMPT_TEMPLATES[name];

  if (!tmpl) throw new Error("Unknown prompt: " + name);

  // Check required arguments
  tmpl.arguments.forEach(function(arg) {
    if (arg.required && !args[arg.name]) {
      throw new Error("Missing required argument: " + arg.name);
    }
  });

  var text = renderConditional(tmpl.template, args);

  return {
    description: name + " prompt",
    messages: [{ role: "user", content: { type: "text", text: text } }]
  };
});

Multi-Step Prompt Chains

Some workflows require multiple prompts in sequence, where each step builds on the previous.

// Multi-step prompt chain for incident investigation
var INCIDENT_CHAIN = {
  name: "investigate-incident",
  description: "Step-by-step incident investigation workflow",
  steps: [
    {
      name: "gather-context",
      prompt: function(args) {
        return "An incident has been reported.\n\n"
          + "Incident: {{description}}\n"
          + "Severity: {{severity|unknown}}\n"
          + "Reported at: {{reportedAt}}\n\n"
          + "First, let's gather context. Please analyze the following logs "
          + "and identify the timeline of events, affected systems, and potential root causes.\n\n"
          + "```\n{{logs}}\n```";
      }
    },
    {
      name: "identify-root-cause",
      prompt: function(args) {
        return "Based on the context gathered in the previous step, "
          + "identify the most likely root cause.\n\n"
          + "Consider:\n"
          + "1. What changed recently? (deployments, config changes, traffic patterns)\n"
          + "2. Are there correlated failures across services?\n"
          + "3. Is this a known failure mode?\n\n"
          + "{{#if metrics}}Additional metrics:\n{{metrics}}\n\n{{/if}}"
          + "Provide your top 3 hypotheses ranked by likelihood.";
      }
    },
    {
      name: "remediation-plan",
      prompt: function(args) {
        return "Based on the identified root cause, create a remediation plan.\n\n"
          + "Include:\n"
          + "1. Immediate mitigation (stop the bleeding)\n"
          + "2. Root cause fix (prevent recurrence)\n"
          + "3. Verification steps (confirm the fix works)\n"
          + "4. Communication template (stakeholder updates)\n"
          + "5. Post-incident tasks (monitoring, documentation)";
      }
    }
  ]
};

// Expose the chain as a prompt with a step argument
server.setRequestHandler("prompts/get", function(request) {
  if (request.params.name === "investigate-incident") {
    var args = request.params.arguments || {};
    var step = parseInt(args.step || "1") - 1;

    if (step < 0 || step >= INCIDENT_CHAIN.steps.length) {
      throw new Error("Invalid step. Available steps: 1-" + INCIDENT_CHAIN.steps.length);
    }

    var stepDef = INCIDENT_CHAIN.steps[step];
    var text = renderConditional(stepDef.prompt(args), args);

    return {
      description: "Incident investigation - Step " + (step + 1) + ": " + stepDef.name,
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: "[Step " + (step + 1) + " of " + INCIDENT_CHAIN.steps.length + ": " + stepDef.name + "]\n\n" + text
          }
        }
      ]
    };
  }
});

Complete Working Example: Project Assistant Prompt Library

var Server = require("@modelcontextprotocol/sdk/server/index.js").Server;
var StdioTransport = require("@modelcontextprotocol/sdk/server/stdio.js").StdioServerTransport;
var fs = require("fs");
var path = require("path");

// ============================================================
// Project Assistant Prompt Library
// Reusable, context-aware prompts for common dev workflows
// ============================================================

var PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();

var server = new Server(
  { name: "project-prompts", version: "1.0.0" },
  { capabilities: { prompts: {} } }
);

// Prompt definitions
var prompts = {
  "code-review": {
    description: "Thorough code review with actionable feedback",
    arguments: [
      { name: "file", required: true, description: "File path relative to project root" },
      { name: "focus", required: false, description: "Focus area: security, performance, readability, or all" }
    ],
    handler: function(args) {
      var filePath = path.join(PROJECT_DIR, args.file);
      if (!fs.existsSync(filePath)) throw new Error("File not found: " + args.file);

      var code = fs.readFileSync(filePath, "utf8");
      var ext = path.extname(args.file).slice(1);
      var focus = args.focus || "all";

      // Try to read package.json for project context
      var projectContext = "";
      var pkgPath = path.join(PROJECT_DIR, "package.json");
      if (fs.existsSync(pkgPath)) {
        var pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
        projectContext = "\nProject: " + pkg.name + " (Node " + (pkg.engines && pkg.engines.node || "any") + ")\n"
          + "Dependencies: " + Object.keys(pkg.dependencies || {}).join(", ") + "\n";
      }

      return {
        description: "Code review: " + args.file + " (" + focus + ")",
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: "Review this " + ext + " file: `" + args.file + "`\n"
              + projectContext
              + "\nFocus: " + focus + "\n\n"
              + "```" + ext + "\n" + code + "\n```\n\n"
              + "Provide specific, actionable feedback. For each issue:\n"
              + "- Quote the problematic code\n"
              + "- Explain the issue and its impact\n"
              + "- Show a corrected version\n\n"
              + "End with an overall quality score (1-10) and a summary."
          }
        }]
      };
    }
  },

  "debug-help": {
    description: "Get help debugging an error with full project context",
    arguments: [
      { name: "error", required: true, description: "Error message or stack trace" },
      { name: "file", required: false, description: "File where the error occurs" }
    ],
    handler: function(args) {
      var messages = [];
      var contextText = "";

      if (args.file) {
        var filePath = path.join(PROJECT_DIR, args.file);
        if (fs.existsSync(filePath)) {
          contextText = "\n\nSource file `" + args.file + "`:\n```\n"
            + fs.readFileSync(filePath, "utf8") + "\n```";
        }
      }

      // Look for relevant config files
      var configFiles = [".env.example", "tsconfig.json", "package.json"];
      var configs = configFiles.filter(function(f) {
        return fs.existsSync(path.join(PROJECT_DIR, f));
      }).map(function(f) {
        return "`" + f + "`: " + fs.readFileSync(path.join(PROJECT_DIR, f), "utf8").substring(0, 500);
      });

      messages.push({
        role: "user",
        content: {
          type: "text",
          text: "I need help debugging this error:\n\n"
            + "```\n" + args.error + "\n```"
            + contextText
            + (configs.length > 0 ? "\n\nProject config:\n" + configs.join("\n\n") : "")
            + "\n\nPlease:\n"
            + "1. Explain the error in plain language\n"
            + "2. Identify the root cause\n"
            + "3. Provide a step-by-step fix\n"
            + "4. Show the corrected code"
        }
      });

      return { description: "Debug: " + args.error.substring(0, 50), messages: messages };
    }
  },

  "generate-api-docs": {
    description: "Generate API documentation from route files",
    arguments: [
      { name: "routeFile", required: true, description: "Express route file to document" },
      { name: "format", required: false, description: "Output format: markdown, openapi, or jsdoc" }
    ],
    handler: function(args) {
      var filePath = path.join(PROJECT_DIR, args.routeFile);
      if (!fs.existsSync(filePath)) throw new Error("Route file not found");

      var code = fs.readFileSync(filePath, "utf8");
      var format = args.format || "markdown";

      return {
        description: "API docs for " + args.routeFile,
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: "Generate " + format + " API documentation for this Express route file:\n\n"
              + "```javascript\n" + code + "\n```\n\n"
              + "For each endpoint, document:\n"
              + "- HTTP method and path\n"
              + "- Description of what it does\n"
              + "- Request parameters, query params, and body schema\n"
              + "- Response format with example\n"
              + "- Error responses\n"
              + "- Authentication requirements"
          }
        }]
      };
    }
  },

  "write-commit-message": {
    description: "Generate a commit message from staged changes",
    arguments: [
      { name: "diff", required: true, description: "Git diff output of staged changes" },
      { name: "style", required: false, description: "Commit style: conventional, simple, or detailed" }
    ],
    handler: function(args) {
      var style = args.style || "conventional";

      var styleGuide = {
        conventional: "Use Conventional Commits format: type(scope): description\n"
          + "Types: feat, fix, docs, style, refactor, perf, test, chore\n"
          + "Keep the first line under 72 characters.",
        simple: "Write a single-line commit message under 72 characters that describes what changed and why.",
        detailed: "Write a commit message with:\n"
          + "- A short summary line (under 72 chars)\n"
          + "- A blank line\n"
          + "- A detailed body explaining what changed and why\n"
          + "- Bullet points for multiple changes"
      };

      return {
        description: "Commit message (" + style + " style)",
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: "Generate a git commit message for these changes:\n\n"
              + "```diff\n" + args.diff + "\n```\n\n"
              + "Style: " + (styleGuide[style] || styleGuide.conventional) + "\n\n"
              + "Return ONLY the commit message, no explanation."
          }
        }]
      };
    }
  },

  "project-overview": {
    description: "Generate a comprehensive overview of the project structure",
    arguments: [],
    handler: function() {
      // Collect project structure
      var structure = [];
      function walk(dir, depth) {
        if (depth > 3) return;
        var entries;
        try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }

        entries.forEach(function(entry) {
          if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") return;
          var rel = path.relative(PROJECT_DIR, path.join(dir, entry.name));
          var indent = "  ".repeat(depth);

          if (entry.isDirectory()) {
            structure.push(indent + rel + "/");
            walk(path.join(dir, entry.name), depth + 1);
          } else {
            var size = fs.statSync(path.join(dir, entry.name)).size;
            structure.push(indent + rel + " (" + formatSize(size) + ")");
          }
        });
      }

      walk(PROJECT_DIR, 0);

      // Read key files
      var keyFiles = {};
      ["package.json", "README.md", "app.js", "index.js"].forEach(function(f) {
        var fp = path.join(PROJECT_DIR, f);
        if (fs.existsSync(fp)) {
          keyFiles[f] = fs.readFileSync(fp, "utf8").substring(0, 1000);
        }
      });

      var contextParts = ["Project structure:\n```\n" + structure.join("\n") + "\n```"];

      Object.keys(keyFiles).forEach(function(f) {
        contextParts.push("`" + f + "`:\n```\n" + keyFiles[f] + "\n```");
      });

      return {
        description: "Project overview",
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: "Provide a comprehensive overview of this project.\n\n"
              + contextParts.join("\n\n")
              + "\n\nCover:\n"
              + "1. What this project does\n"
              + "2. Technology stack\n"
              + "3. Key files and their purposes\n"
              + "4. How to set it up and run it\n"
              + "5. Architecture patterns used"
          }
        }]
      };
    }
  }
};

function formatSize(bytes) {
  if (bytes < 1024) return bytes + "B";
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + "KB";
  return (bytes / 1048576).toFixed(1) + "MB";
}

// Register handlers
server.setRequestHandler("prompts/list", function() {
  return {
    prompts: Object.keys(prompts).map(function(name) {
      return {
        name: name,
        description: prompts[name].description,
        arguments: prompts[name].arguments
      };
    })
  };
});

server.setRequestHandler("prompts/get", function(request) {
  var name = request.params.name;
  var args = request.params.arguments || {};

  if (!prompts[name]) throw new Error("Unknown prompt: " + name);

  // Validate required arguments
  (prompts[name].arguments || []).forEach(function(arg) {
    if (arg.required && !args[arg.name]) {
      throw new Error("Missing required argument: " + arg.name);
    }
  });

  return prompts[name].handler(args);
});

var transport = new StdioTransport();
server.connect(transport).then(function() {
  console.error("Project prompts MCP server running");
  console.error("  Project: " + PROJECT_DIR);
  console.error("  Available prompts: " + Object.keys(prompts).join(", "));
});

Common Issues & Troubleshooting

Prompt Returns Empty Messages Array

The messages array must contain at least one message:

// Wrong: empty messages
return { description: "test", messages: [] };

// Right: always include at least one message
return {
  description: "test",
  messages: [{ role: "user", content: { type: "text", text: "..." } }]
};

Dynamic File Content Contains Special Characters

File content with backticks, template literals, or other markdown-conflicting characters can break prompt formatting:

// Escape backticks in code blocks
function escapeForCodeBlock(content) {
  // Replace triple backticks with escaped version
  return content.replace(/```/g, "\\`\\`\\`");
}

// Or use different delimiters
var text = "Code:\n<code>\n" + content + "\n</code>";

Prompt Arguments Not Validated

Always validate arguments before using them in file paths or queries:

function validateFilePath(relativePath) {
  var fullPath = path.join(PROJECT_DIR, relativePath);
  var resolved = path.resolve(fullPath);

  if (!resolved.startsWith(path.resolve(PROJECT_DIR))) {
    throw new Error("Path traversal detected");
  }

  if (!fs.existsSync(resolved)) {
    throw new Error("File not found: " + relativePath);
  }

  return resolved;
}

Prompt Template Rendering Fails Silently

Unreplaced template variables stay in the output. Add a warning:

function renderWithWarnings(template, variables) {
  var result = renderTemplate(template, variables);
  var unreplaced = result.match(/\{\{[^}]+\}\}/g);
  if (unreplaced) {
    console.error("Warning: unreplaced template variables: " + unreplaced.join(", "));
  }
  return result;
}

Best Practices

  • Keep prompts focused on a single workflow — A prompt that tries to review code, write tests, and generate docs all at once is too complex. Split into separate prompts.
  • Include project context automatically — Read package.json, config files, and directory structure to give the model context without requiring the user to provide it manually.
  • Use arguments for customization, not for essential content — Required arguments should be the minimum needed. Optional arguments customize behavior (format, focus, style).
  • Validate file paths against the project directory — Never allow prompts to read files outside the project root. Path traversal is a real risk.
  • Return descriptive descriptions — The description field in the response should reflect the specific prompt instance, not just the generic template name.
  • Support multiple output formats — Where applicable, let users choose between markdown, JSON, YAML, or other formats through a format argument.
  • Document your prompts with examples — Include example argument values in the argument descriptions so users know what to provide.
  • Test prompts with real project data — A prompt that works with sample code may fail with real files containing edge cases, large content, or unexpected formats.

References

Powered by Contentful