n4nAI

MCP resources vs tools vs prompts: the three primitives

Defends MCP resources, tools, and prompts—the three core mcp primitives—with examples and misconceptions for engineers building LLM agents.

n4n Team3 min read714 words

Audio narration

Coming soon — every post will get a voice note here.

The Model Context Protocol standardizes how LLM applications exchange context with external systems. It defines three mcp primitives—resources, tools, and prompts—that separate passive data, executable actions, and reusable instruction templates. Understanding the boundaries between these primitives determines whether your agent architecture stays maintainable or collapses into prompt soup.

How MCP primitives work

MCP runs over JSON-RPC 2.0, typically via stdio or Server-Sent Events. A client connects to a server, negotiates capabilities, and lists what the server exposes. The three mcp primitives are orthogonal: each solves a different problem in the loop between a model and the outside world.

Resources: read-only context

Resources are identified by URI and return content on fetch. They are strictly read-only. A resource might point to a file, a database row, or a live API snapshot. The server decides how to resolve the URI; the client does not assume it is a local path.

{
  "uri": "postgres://prod/users?limit=10",
  "contents": [
    { "text": "[{\"id\":1,\"name\":\"alice\"},{\"id\":2,\"name\":\"bob\"}]" }
  ]
}

The model never directly calls a resource. The orchestration layer reads it and injects the payload into the conversation. This keeps side-effect-free data fetching out of the tool namespace.

Tools: executable actions

Tools are functions with a name, description, and JSON Schema input. They are the only primitive permitted to mutate state or trigger external side effects. The model emits arguments; the client validates against the schema and dispatches the call.

{
  "name": "create_ticket",
  "description": "Open a bug ticket in the tracker",
  "inputSchema": {
    "type": "object",
    "properties": {
      "title": { "type": "string" },
      "severity": { "type": "string", "enum": ["low", "high"] }
    },
    "required": ["title"]
  }
}

A tool call returns a result object that the client feeds back to the model. Timeouts, retries, and permission checks belong to the client or server, not the protocol definition.

Prompts: parameterized message templates

Prompts are server-defined message factories. They accept typed arguments and return a list of messages ready to seed a conversation. They are not executed against external systems; they shape the text sent to the model.

{
  "name": "summarize_incident",
  "arguments": [{ "name": "incident_id", "required": true }],
  "messages": [
    {
      "role": "user",
      "content": {
        "text": "Summarize incident {{incident_id}} with impact and remediation steps."
      }
    }
  ]
}

Prompts let a server encode domain expertise—say, a specific code-review rubric—without hardcoding it in every client.

Why the distinction matters

Mixing these concerns produces brittle agents. The separation of mcp primitives gives you three concrete wins:

  • Security boundary. Read-only data and state-changing actions live in different namespaces. You can grant a client resource-read without tool-execute.
  • Context control. Resources are fetched on demand, so you avoid stuffing every document into the prompt. Tools stay out of the context until called.
  • Composability. A single agent can mount five MCP servers, each exposing a subset of primitives, and treat them uniformly.

When you treat a resource like a tool, you invite accidental writes. When you bury a prompt inside a tool response, you lose reusability. The protocol’s value is the discipline it enforces.

Concrete example: a code-review assistant

Consider an MCP server that helps an LLM review a pull request. It exposes one of each primitive:

import { Server } from "@modelcontextprotocol/sdk/server";

const server = new Server({ name: "repo-agent", version: "0.1" });

server.setRequestHandler("resources/list", async () => ({
  resources: [
    { uri: "git://pr/123/diff", name: "PR 123 diff", mimeType: "text/plain" }
  ]
}));

server.setRequestHandler("tools/list", async () => ({
  tools: [
    {
      name: "post_comment",
      inputSchema: {
        type: "object",
        properties: { line: { type: "number" }, body: { type: "string" } },
        required: ["line", "body"]
      }
    }
  ]
}));

server.setRequestHandler("prompts/list", async () => ({
  prompts: [
    {
      name: "review_pr",
      arguments: [{ name: "pr_number", required: true }],
      messages: [
        { role: "user", content: { text: "Review PR {{pr_number}} for security and style." } }
      ]
    }
  ]
}));

A client orchestrating this might first pull the review_pr prompt to set the task, read the git://pr/123/diff resource to get the code, then call post_comment when the model identifies an issue. The three mcp primitives cooperate without overlapping responsibilities.

# Client-side orchestration (using a hypothetical MCP session API)
async def review(pr_num: int):
    messages = await session.get_prompt("review_pr", {"pr_number": pr_num})
    diff = await session.read_resource(f"git://pr/{pr_num}/diff")
    messages.append({"role": "user", "content": diff.contents[0].text})

    response = await llm.chat(messages)
    if response.tool_call:
        await session.call_tool("post_comment", response.tool_args)

This pattern keeps the LLM’s context clean and the side effects explicit.

Common misconceptions

“Resources are just files.” A resource URI can map to a live query or a computed view. The postgres:// example above is not a file on disk. Treating resources as static files ignores their dynamic potential.

“Prompts are system messages.” Prompts are full message arrays with arguments. They can include few-shot examples, multi-turn scaffolds, or role-playing setups. Reducing them to a single system string throws away their structure.

“Tools are OpenAI function calling.” MCP tools wrap function calling, but they are transport-agnostic and vendor-neutral. A single tool definition works whether the backend model is from Anthropic, OpenAI, or a local runtime. The primitive is the contract, not the model API.

“You must implement all three.” A server can expose only resources (a read-only data bridge) or only tools (an action gateway). Prompts are optional sugar. Forcing all three into every server creates noise.

“The model chooses primitives directly.” In practice, the client mediates. The model requests a tool call; the client verifies and executes. The model may suggest a resource is needed, but the client fetches it. Prompts are usually selected by the application, not the model.

The mcp primitives give engineers a vocabulary for agent design. Use resources for anything you would GET, tools for anything you would POST, and prompts for anything you would otherwise copy-paste into every request.

Tagsmcpmcp-toolsmcp-resourcesdefinition

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All model context protocol (mcp) deep dives posts →