Skip to content

Model Context Protocol (MCP): Connect AI to Your Tools and Data · Primitives, client features and transports · lesson 3 of 18 · 15 min

Server primitives: tools, resources and prompts

Three primitives, three controllers

MCP servers expose three kinds of things, and the most useful way to remember them is who controls when they are used:

| Primitive | Controlled by | What it is | Example | |---|---|---|---| | Tools | The model | Functions the model may call, with input (and optional output) schemas | search_contacts, create_draft_campaign | | Resources | The application | Read-only data identified by URIs that the host can attach as context | crm://contacts/c_101, brand://guidelines | | Prompts | The user | Reusable templates the user picks (often as slash commands) | /weekly-report, /audit-landing-page |

Choosing the right primitive is the first design decision for every capability.

Tools

A tool has a name, human-readable title, description, an inputSchema (JSON Schema; 2026-07-28 allows any JSON Schema 2020-12 keywords), an optional outputSchema, and optional annotations.

  • Results contain content (text, images, audio, resource links or embedded resources) and optionally structuredContent that matches outputSchema. For backwards compatibility, a tool returning structured content should also return the serialized JSON as text.
  • Errors: tool execution errors are returned as results with isError: true so the model can self-correct; protocol errors (unknown tool, malformed request) are JSON-RPC errors.
  • Annotations are hints: readOnlyHint, destructiveHint, idempotentHint, openWorldHint, plus title. Hosts may use them to decide when to ask for confirmation. The spec is explicit that clients must treat annotations as untrusted unless the server is trusted; a malicious server can claim anything.
  • Naming: 1–128 characters of letters, digits, underscore, hyphen and dot; unique within a server; case-sensitive. Hosts may namespace tools from different servers to avoid collisions.
  • Ordering: servers should return tools in a deterministic order so client caches and LLM prompt caches stay stable.

Resources

Resources are addressed by URIs (file:///, https://, or custom schemes like crm://). A server can list concrete resources and resource templates with parameters (crm://contacts/{contact_id}). Contents are text or binary (base64 blob) with a MIME type. Resource annotations can indicate audience (user, assistant), priority and last-modified time.

Use resources when the application or user should decide what context to include: attaching a brief, a dataset summary, or a document to a conversation. Hosts vary in how they surface resources (pickers, @-mentions, automatic attachment), so check your target clients. Change notifications let clients know when lists or subscribed resources change; in 2026-07-28 these flow over a subscriptions/listen stream the client opts into.

Prompts

Prompts are named templates with arguments that return a list of messages, possibly embedding resources or images. They shine for repeatable workflows where a human initiates the action: "Run the monthly SEO audit for {site}" can embed the audit checklist as a resource and fill in the site. Completions (argument autocompletion) make them pleasant to use.

Choosing the primitive: a decision guide

  • Should the model decide when to use it, possibly repeatedly in a loop? → Tool.
  • Is it data a user or app would attach deliberately, and reading it has no side effects? → Resource (and consider also exposing a read tool, because many hosts are tool-first).
  • Is it a workflow the user starts? → Prompt.

Worked example: an e-commerce brand in Dubai

A CRM/marketing server for a UAE fashion brand:

  • Tools: search_customers(query, segment?) (read-only), get_customer(customer_id), create_campaign_draft(name, segment_id, channel) (not destructive, idempotent with a client-supplied key), pause_campaign(campaign_id) (destructive hint true).
  • Resources: brand://tone-of-voice (Arabic and English guidelines), segments://{segment_id}/summary.
  • Prompts: /ramadan-campaign-brief(segment) and /win-back-email(customer_id) that embed the tone-of-voice resource.

The host shows prompts as slash commands to marketers, lets them attach segment summaries, and lets the model call tools during analysis, asking for confirmation before pause_campaign.

Hands-on: all three primitives in Python (SDK v2)

from typing import Annotated
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations

mcp = MCPServer("brand-crm", instructions="Search before drafting. Never invent customer data.")

CUSTOMERS = {"cu_1": {"name": "Layla A.", "segment": "vip", "city": "Dubai"}}

class Customer(BaseModel):
    customer_id: str
    name: str
    segment: str
    city: str

@mcp.tool(title="Get customer", annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False))
def get_customer(customer_id: Annotated[str, Field(description="ID like cu_1, from search_customers")]) -> Customer:
    """Fetch one customer's profile by ID."""
    c = CUSTOMERS.get(customer_id)
    if not c:
        raise ToolError(f"No customer {customer_id!r}; call search_customers first.")
    return Customer(customer_id=customer_id, **c)

@mcp.resource("brand://tone-of-voice", mime_type="text/markdown")
def tone_of_voice() -> str:
    """Brand tone-of-voice guidelines (English and Arabic)."""
    return "## Tone\nWarm, confident, never pushy. Avoid slang. Arabic copy uses Modern Standard Arabic."

@mcp.prompt(title="Win-back email")
def win_back_email(customer_id: str) -> str:
    """Draft a win-back email for a lapsed customer."""
    return (f"Using the brand tone-of-voice resource, draft a short win-back email for customer {customer_id}. "
            "Call get_customer first. Offer no discount above 10%.")

if __name__ == "__main__":
    mcp.run()

Returning a Pydantic model gives the tool an outputSchema and structuredContent automatically; ToolError becomes an isError result the model can read.

Pitfalls

  • Exposing everything as tools, including large reference documents better served as resources.
  • Trusting annotations from third-party servers.
  • Returning only unstructured text when a schema would make results reliable.
  • Non-deterministic tool ordering that defeats caching.

Measuring success

Track which primitives your target hosts actually surface and use, tool-call error rates, and how often users invoke prompts; retire unused capabilities.

Video lecture: Server primitives: tools, resources and prompts

Lecture coming soon · 15 chapters · about 9 minutes. Read the full transcript below.

  1. Server primitives
  2. Why primitives matter
  3. Who's in control?
  4. Tools in detail
  5. Annotations are hints
  6. Simple example: a designer's portfolio server
  7. Resources and prompts
  8. Decision guide
  9. Example: Dubai fashion brand
  10. Hands-on: all three in Python
  11. Resource and tool?
  12. Deeper: Ramadan brief workflow
  13. Watch me do it: brand-crm server
  14. Try this now
  15. Recap

Lecture transcript

Server primitives

Every capability you expose through MCP is one of three things: a tool, a resource or a prompt. Picking the wrong one leads to awkward, unreliable integrations. In this lesson you'll learn what each primitive is, who controls it, and how to choose, with a working Python server that uses all three.

Why primitives matter

Why does picking the right primitive matter? Because it changes who's in control and how the host presents your capability. Put a two hundred page brand manual behind a tool, and the model may call it on every turn, burning tokens. Offer it as a resource, and the user attaches it when it matters. Hide a common workflow inside a tool description, and nobody finds it; offer it as a prompt, and it appears as a slash command. Think of a hotel: room service, the minibar and the concierge each serve different moments.

Who's in control?

The easiest way to remember them is by who's in control. Tools are controlled by the model. They're functions it may call, like search contacts or create a draft campaign. Resources are controlled by the application. They're read only data addressed by URIs that the host can attach as context, like a customer record or brand guidelines. Prompts are controlled by the user. They're reusable templates people pick, often as slash commands, like weekly report or audit this landing page.

Tools in detail

Let's go deeper on tools. A tool has a name, a readable title, a description, an input schema and, optionally, an output schema and annotations. Results contain content, like text or images, and optionally structured content that matches the output schema. If something goes wrong while executing, return a result with is error set, so the model can correct itself. Save protocol errors for things like unknown tools or malformed requests. And return tools in a deterministic order, so clients and model prompt caches stay stable.

Annotations are hints

Annotations deserve a warning. They're hints like read only, destructive, idempotent and open world. Hosts may use them to decide when to ask the user for confirmation. But the spec is explicit: clients must treat annotations as untrusted unless the server itself is trusted. A malicious server can label a delete everything tool as read only. So annotations help good servers communicate. They're not a security control.

Simple example: a designer's portfolio server

A simple example. A freelance designer builds an MCP server for their portfolio. A tool, search projects by industry, lets the model find relevant case studies during a conversation. A resource, the rate card, can be attached when a client asks about pricing, and it never changes mid conversation. A prompt, write a proposal for a client and industry, gives the designer a one click starting point that pulls the right case studies and the rate card. Three primitives, each doing exactly one job.

Resources and prompts

Resources are addressed by URIs, like file, https, or custom schemes such as crm colon slash slash. Servers can list concrete resources and resource templates with parameters, like a contact by id. Contents are text or binary with a MIME type. Use resources when a person or the app should decide what context to include, like attaching a brief or a dataset summary. Prompts are named templates with arguments that return messages, and can embed resources. They shine for repeatable, human initiated workflows.

Decision guide

Here's a quick decision guide. Should the model decide when to use it, perhaps repeatedly? Make it a tool. Is it data someone attaches on purpose, with no side effects? Make it a resource, and consider also offering a read tool, because many hosts are tool first. Is it a workflow the user kicks off? Make it a prompt. One more tip: check how your target hosts surface resources and prompts, because support and presentation vary between clients.

Example: Dubai fashion brand

A worked example. A fashion brand in Dubai exposes a CRM and marketing server. Tools: search customers, get customer, create campaign draft, and pause campaign, which carries a destructive hint so the host asks first. Resources: the brand's tone of voice guide in English and Arabic, and segment summaries. Prompts: a Ramadan campaign brief and a win back email, both embedding the tone of voice resource. Marketers see the prompts as slash commands, attach segment summaries, and the model calls tools during analysis.

Hands-on: all three in Python

The hands on server implements all three in Python with SDK version two. The get customer tool returns a Pydantic model, so the SDK generates an output schema and structured content automatically. If the id isn't found, it raises a tool error, which becomes an is error result with a helpful hint. The tone of voice resource returns markdown. And the win back prompt tells the model to call get customer first and caps discounts at ten percent. Avoid the classic pitfalls: everything as a tool, trusting third party annotations, text only results, and random tool order.

Resource and tool?

A question people ask: can the same capability be both a resource and a tool? Yes, and it's often wise. A brand guide could be a resource, so users can attach it deliberately, and also a read brand guide tool, so tool first hosts and autonomous agents can fetch it when needed. Keep them consistent by having both call the same function. Just avoid duplicating everything; offer the dual form only for high value context that different hosts surface differently.

Deeper: Ramadan brief workflow

Let's deepen the Dubai fashion brand example. The marketing team runs a Ramadan campaign every year. With the server in place, a marketer types the Ramadan campaign brief prompt, picks the VIP segment, and the host automatically embeds the Arabic and English tone of voice guide. The model then calls search customers and get customer to understand the segment, and drafts the brief. When it suggests pausing last year's underperforming campaign, the destructive hint on pause campaign makes the host ask for confirmation. Illustrative result: first drafts that used to take a morning now take under an hour including review, and the tone of voice is consistent because it comes from one resource.

Watch me do it: brand-crm server

Watch me do it. Let's walk through the brand CRM server. The server is created with instructions telling hosts to search before drafting and never invent customer data. The Customer model has id, name, segment and city. The get customer tool has a title, read only annotations, and a customer id parameter with a description pointing to search customers. If the id isn't in the dictionary, it raises a tool error that tells the model what to do next. Because it returns a Customer model, the SDK publishes an output schema and returns structured content. The tone of voice resource has a fixed URI and markdown MIME type. The win back email prompt takes a customer id and returns an instruction that tells the model to call get customer first and caps any discount at ten percent. I open it in the Inspector: one tool, one resource, one prompt. Calling get customer with cu underscore one returns structured data; with a bad id, a helpful error.

Try this now

Try this now. Pick one system you plan to expose. On a single page, list three tools, two resources and one prompt. For each, write one sentence explaining why it's that primitive, using the controller test: does the model decide, does the app or user attach it, or does the user start it? Then check your target hosts' documentation: do they surface resources and prompts? If not, decide whether a read tool should mirror your most important resource.

Recap

To recap: tools for the model, resources for the application, prompts for the user. Use schemas and structured results, return errors the model can act on, treat annotations as hints, and keep tool order stable. Your next step: pick one system you plan to expose, and list three tools, two resources and one prompt, with a sentence justifying each choice.

Key takeaways

  • Tools are model-controlled, resources are application-controlled, prompts are user-controlled.
  • Tools have input and optional output schemas; return structuredContent plus text, and isError for execution failures.
  • Tool annotations are hints and must be treated as untrusted unless the server is trusted.
  • Resources are URI-addressed read-only context; prompts are user-initiated workflow templates.
  • Return tools in a deterministic order to keep client and LLM caches stable.

Try it

For one system you plan to expose, list three tools, two resources and one prompt, and justify each primitive choice using the decision guide.