Skip to main content
MCP Deep Dive

Tools, Resources, Prompts

Ravinder··5 min read
MCPModel Context ProtocolAIToolsResourcesPrompts
Share:
Tools, Resources, Prompts

Most MCP server implementations I review make the same mistake on day one: they put everything into tools. Database reads, file lookups, pre-built prompt templates — all crammed into callTool. It works, but you are throwing away the protocol's most useful design constraint. MCP ships three distinct primitives for a reason, and conflating them costs you discoverability, caching, and model-side UX.

The Three Primitives at a Glance

Before diving into each one, here is how they relate:

graph TD Client["MCP Client / Agent"] T["Tools\n(side-effectful actions)"] R["Resources\n(read-only data)"] P["Prompts\n(reusable templates)"] Client -- "tools/call" --> T Client -- "resources/read" --> R Client -- "prompts/get" --> P T -- "mutates state" --> External["External Systems"] R -- "reads state" --> External P -- "returns messages[]" --> Client

The separation is intentional. Resources are meant to be cached and paginated. Tools carry side effects. Prompts are composable message templates the model can invoke by name.

Tools: Actions with Side Effects

A tool is anything that changes state or triggers a process. Think send_email, create_ticket, run_query, deploy_service. The model calls it, something happens in the world.

Tool definitions live in tools/list and are invoked via tools/call. The schema is a JSON Schema object that the model uses to decide how to call it.

// tools/list response shape
{
  tools: [
    {
      name: "create_ticket",
      description: "Create a support ticket in the project tracker.",
      inputSchema: {
        type: "object",
        properties: {
          title:    { type: "string" },
          priority: { type: "string", enum: ["low", "medium", "high"] },
          body:     { type: "string" }
        },
        required: ["title", "body"]
      }
    }
  ]
}

The handler:

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name === "create_ticket") {
    const { title, priority = "medium", body } = req.params.arguments as any;
    const ticket = await tracker.create({ title, priority, body });
    return {
      content: [{ type: "text", text: `Ticket #${ticket.id} created.` }]
    };
  }
  throw new Error("Unknown tool");
});

Rule of thumb: if the operation would belong behind a POST, PUT, or DELETE in a REST API, it is a tool.

Resources: Stable, Readable Data

Resources represent data the model should be able to read without triggering a side effect. Documentation pages, configuration files, database records, knowledge-base articles. The key property is that they are addressable by URI.

// resources/list response
{
  resources: [
    {
      uri: "db://customers/acme-corp",
      name: "ACME Corp customer record",
      mimeType: "application/json"
    },
    {
      uri: "docs://runbook/deployment",
      name: "Deployment runbook",
      mimeType: "text/markdown"
    }
  ]
}

Reading a resource:

server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
  const { uri } = req.params;
  if (uri.startsWith("db://customers/")) {
    const id = uri.replace("db://customers/", "");
    const record = await db.customers.findOne(id);
    return {
      contents: [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify(record, null, 2)
        }
      ]
    };
  }
  throw new Error("Resource not found");
});

Because resources are read-only by contract, clients can safely cache them. Many MCP clients will also surface them in a separate "context" panel rather than in the tool-call flow — giving users visibility into what data the model is working with.

Rule of thumb: if it maps to a GET with a stable URL, it is a resource.

Prompts: Reusable Message Templates

Prompts are the least-understood primitive. They are not prompts in the sense of raw strings you stuff into a system message. They are named, parameterised message sequences that the server owns and the client can invoke by name.

// prompts/list response
{
  prompts: [
    {
      name: "triage_ticket",
      description: "Produce a triage assessment for a support ticket.",
      arguments: [
        { name: "ticket_id", required: true },
        { name: "tone",      required: false }
      ]
    }
  ]
}

Getting the rendered messages:

server.setRequestHandler(GetPromptRequestSchema, async (req) => {
  if (req.params.name === "triage_ticket") {
    const { ticket_id, tone = "professional" } = req.params.arguments ?? {};
    const ticket = await tracker.get(ticket_id);
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `You are a ${tone} support engineer. Triage this ticket:\n\n${JSON.stringify(ticket, null, 2)}`
          }
        }
      ]
    };
  }
  throw new Error("Unknown prompt");
});

Why does this matter? Because the business logic for how to talk to the model about a ticket now lives in the server, not scattered across every client. When your triage criteria change, you update one place.

Decision Matrix

flowchart LR Q1{"Does it mutate\nexternal state?"} Q2{"Is it addressable\nby a stable URI?"} Q3{"Is it a reusable\nmessage pattern?"} Tool["Use a Tool"] Resource["Use a Resource"] Prompt["Use a Prompt"] Q1 -- Yes --> Tool Q1 -- No --> Q2 Q2 -- Yes --> Resource Q2 -- No --> Q3 Q3 -- Yes --> Prompt Q3 -- No --> Resource

If you are still unsure, ask: who owns the lifecycle? Tools are stateless handlers — the client drives them. Resources are server-owned, addressable content. Prompts are server-owned conversational scaffolding.

Mixing Primitives in Practice

A real SaaS integration will use all three. Consider a customer-success agent:

  • Resource crm://accounts/{id} — read the account record.
  • Tool create_follow_up_task — schedule a callback.
  • Prompt draft_renewal_email — generate a renewal message in the company's voice.

None of those belong in the same category, and none of them should be collapsed into a single mega-tool called do_crm_thing.

Key Takeaways

  • Tools are for side-effectful operations; treat them like POST/PUT/DELETE endpoints.
  • Resources are stable, read-only, URI-addressed data; clients may cache them.
  • Prompts are server-owned message templates that encode reusable reasoning patterns.
  • Conflating all three into tools creates a brittle, un-cacheable, undiscoverable server.
  • The decision matrix is simple: mutation → tool, stable read → resource, reusable conversation shape → prompt.
  • Prompt templates shift prompt-engineering ownership to the server, enabling centralised updates without client changes.
Share: