AI Agents

MCP Resources and Prompts: Beyond Tool Registration

MCP resources expose readable content to AI agents; MCP prompts inject reusable instructions.

The Model Context Protocol defines three primitives: tools, resources, and prompts. Tools get all the attention because they are the most versatile - an agent calls a tool, the tool does something, the result comes back. But tools are poorly suited to two common needs: exposing large, read-only content that an agent should be able to browse (a code file, a database schema, an API spec) and injecting reusable instructions that appear in a specific part of the conversation without occupying a tool call slot. Resources and prompts fill exactly these gaps, and understanding when to reach for each over a tool produces cleaner, more efficient agent integrations.

MCP resource
An MCP resource is a URI-addressable piece of content exposed by an MCP server that an AI client can read and embed in its context - analogous to a file the agent can open, unlike a tool which the agent invokes to perform an action, suitable for static or slowly-changing content like schemas, documentation, and configuration files.

MCP resources - what they are and when to use them

A resource is anything with a URI and readable content: a file, a database row, an API endpoint's documentation, a configuration value. The agent client (Claude Code or a custom orchestrator) reads the resource and uses its content as context. Resources are appropriate when:

  • The content is large and the agent needs to read it without triggering a full tool-call round-trip
  • The content is read-only - the agent consumes it but does not modify it
  • Multiple agents or sessions need access to the same content without each executing a tool call to fetch it
  • The content changes infrequently (schema definitions, API documentation, project configuration)

Resources are a poor fit for: content that requires computation to generate, content that changes per-request based on parameters, or content that an agent should write to (use a tool for that).

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


const server = new McpServer({
  name: 'project-context',
  version: '1.0.0',
});

// Static resource - database schema documentation
server.resource(
  'db-schema',
  'postgres://internal/schema',
  {
    name: 'Database Schema',
    description: 'Complete PostgreSQL schema including all tables, columns, constraints, and relationships.',
    mimeType: 'text/plain',
  },
  async (uri) => {
    const schema = readFileSync('./docs/schema.sql', 'utf8');
    return {
      contents: [{
        uri: uri.toString(),
        mimeType: 'text/plain',
        text: schema,
      }],
    };
  }
);

// Dynamic resource - live API spec fetched at read time
server.resource(
  'api-spec',
  new URL('openapi://internal/spec'),
  {
    name: 'OpenAPI Specification',
    description: 'Current OpenAPI 3.0 spec for the internal REST API.',
    mimeType: 'application/json',
  },
  async (uri) => {
    const spec = await fetchCurrentApiSpec();
    return {
      contents: [{
        uri: uri.toString(),
        mimeType: 'application/json',
        text: JSON.stringify(spec, null, 2),
      }],
    };
  }
);

// Resource template - parameterised: reads any table by name
server.resource(
  'table-data',
  new URL('postgres://internal/tables/{name}'),
  {
    name: 'Table Contents',
    description: 'Read the first 100 rows of any named table.',
    mimeType: 'application/json',
  },
  async (uri) => {
    const tableName = uri.pathname.split('/').pop();
    if (!tableName) throw new Error('Table name required');
    const rows = await queryTable(tableName, 100);
    return {
      contents: [{
        uri: uri.toString(),
        mimeType: 'application/json',
        text: JSON.stringify(rows, null, 2),
      }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

MCP prompts - reusable agent instructions

An MCP prompt is a named, parameterised instruction template that a client can invoke to get a pre-constructed set of messages. The client presents available prompts to the user (in Claude Code, these appear as slash commands), the user selects one with optional parameters, and the resulting messages are injected into the conversation. Prompts are appropriate when you have a repeatable task pattern that always starts with the same context and instructions, the instructions depend on parameters that vary per invocation, or you want to standardise how agents approach a domain-specific task.

// MCP prompt for a standardised code review workflow
server.prompt(
  'code-review',
  'Perform a structured code review on the specified files.',
  {
    files: {
      type: 'string',
      description: 'Comma-separated list of file paths to review.',
      required: true,
    },
    focus: {
      type: 'string',
      description: 'Review focus: "security" | "performance" | "correctness" | "all"',
      required: false,
    },
  },
  async ({ files, focus = 'all' }) => {
    const fileList = files.split(',').map(f => f.trim());
    const fileContents = await Promise.all(
      fileList.map(async (path) => ({
        path,
        content: readFileSync(path'utf8'),
      }))
    );

    const fileSection = fileContents
      .map(f => '--- ' + f.path + ' ---
' + f.content)
      .join('

');

    const checklist = [
      focus === 'security' || focus === 'all'
        ? '- SQL injection, XSS, auth bypass, secret exposure'
        : '',
      focus === 'performance' || focus === 'all'
        ? '- N+1 queries, unnecessary re-renders, memory leaks'
        : '',
      focus === 'correctness' || focus === 'all'
        ? '- Off-by-one errors, null handling, edge cases'
        : '',
    ].filter(Boolean).join('
');

    const text = [
      'Perform a ' + focus + ' code review on the following files.',
      '',
      'Files to review:',
      fileSection,
      '',
      'Review checklist for ' + focus + ':',
      checklist,
      '',
      'For each issue: state file:line, severity (Critical/High/Medium/Low), description, and fix.',
    ].join('
');

    return {
      messages: [{
        role: 'user' as const,
        content: { type: 'text' as const, text },
      }],
    };
  }
);

// Simpler prompt - parameter injection without file access
server.prompt(
  'explain-error',
  'Get a structured explanation of an error message with fix suggestions.',
  {
    error: {
      type: 'string',
      description: 'The full error message or stack trace.',
      required: true,
    },
    language: {
      type: 'string',
      description: 'Programming language context, e.g. "TypeScript" or "Python".',
      required: false,
    },
  },
  async ({ error, language = 'TypeScript' }) => {
    const text = [
      'Explain this ' + language + ' error and provide a fix:',
      '',
      error,
      '',
      'Format your response as:',
      '1. Root cause (one sentence)',
      '2. Why it happens (2-3 sentences)',
      '3. Fix (with code example if applicable)',
      '4. Prevention (how to avoid this class of error)',
    ].join('
');

    return {
      messages: [{
        role: 'user' as const,
        content: { type: 'text' as const, text },
      }],
    };
  }
);

Resources vs tools vs prompts - the decision guide

Use this to decide which primitive fits a given need:

  • Tool - the agent needs to do something: write a file, call an API, execute a query, send a message. The agent initiates the action; your server performs it.
  • Resource - the agent needs to read something: a schema, a document, a configuration. The content exists independently and the agent consumes it.
  • Prompt - the human needs a shortcut: a standardised way to invoke a complex, parameterised instruction pattern without typing it out. The prompt constructs the conversation; the agent then acts on it.

Failure modes with resources and prompts

  • Resource content too large - a resource that returns 200,000 characters of a log file will fill the agent's context window. Cap resource content at 20,000 to 50,000 characters and expose a parameterised template resource that returns a slice (by line range or date range) instead.
  • Prompt messages that conflict with system prompts - prompts inject user-role messages, not system messages. They can conflict with an existing system prompt's instructions. Test prompt invocations with the system prompt your agent already has in place.
  • URI scheme collisions - if you register multiple resources with similar URI templates, the client's URI matching may route to the wrong handler. Use distinct, unambiguous URI schemes per resource type.

For the tools layer that complements resources and prompts, see advanced tool use patterns. For writing the agent definitions in Claude Code that invoke these MCP primitives, see the Claude Code subagents guide.