Guide

An MCP server can expose three kinds of capability. Tools do work. Resources are readable data. Prompts are reusable templates. Mixing them up is how servers either dump megabytes into context or force the model to guess names it could have picked from a list.

Concept

Before you start: a tool is a named action with a JSON Schema for its inputs, the same idea as function calling. MCP messages use JSON-RPC 2.0: typed requests such as tools/list and tools/call.

The big idea

PrimitiveWho starts itPurpose
ToolsModel, via the hostSide-effecting or computed actions
ResourcesApp or user, sometimes model-assistedReadable data at a URI (file://…, postgres://…)
PromptsUser or appTemplates and workflows the host can insert

Tools need the same discipline as any agent tool: a clear description, pagination, compact outputs. Resources separate data from actions. Hosts can show them in a picker or stuff selected content into context, without a read_whatever tool for every path. Prompts package canned workflows ("triage this sev") and stay user-triggered, so control stays explicit.

Some servers also support sampling (the server asks the host to run a completion) and elicitation (the server asks the host to collect user input). Those invert the usual trust direction. Treat them as power features that need host policy.

Walking through a metrics server

Imagine a metrics connector.

  • Resource metrics://services — a list of service names for the UI picker.
  • Resource metrics://services/{name}/dashboard — a markdown summary the user can attach.
  • Tool query_range — {service, metric, start, end} → compact JSON.

If you only expose query_range, the model guesses service names and may scan blindly. If you only expose giant resources, you dump megabytes into context. The split lets the host show a picker while the model still runs precise queries.

A billing tool looks the same as function calling. This is a sketch of the handler, not a full SDK server:

Python
def get_subscription(customer_id):
    catalog = {"cus_123": {"plan": "pro", "status": "active", "renews_on": "2026-11-01"}}
    sub = catalog.get(customer_id)
    if not sub:
        return {"error": "not_found", "customer_id": customer_id}
    return sub

print(get_subscription("cus_123"))
print(get_subscription("cus_999"))
{'plan': 'pro', 'status': 'active', 'renews_on': '2026-11-01'}
{'error': 'not_found', 'customer_id': 'cus_999'}

The host lists the tool, the model selects get_subscription, the host calls the server, and the JSON re-enters the conversation as a tool result.

Design notes that matter once several hosts share the server:

  • Prefer task-level tools over a generic proxy_http. Hosts load all enabled tools into context.
  • Namespace names if hosts merge servers (billing_get_subscription).
  • Bump the server version when behavior changes. Hosts and evals cache assumptions.

Common mistakes

  • One kitchen-sink tool. A generic HTTP proxy pollutes every session and hides the real job.
  • Resources that are too big. A whole database dump is not a resource the model should read.
  • No pagination on lists. Long results crowd out the rest of the prompt.

Check yourself

  • Why not expose only query_range for metrics? The model then has to guess service names instead of picking them from a catalog resource.
  • Who should trigger an MCP prompt template? The user or the app, so the workflow stays an explicit choice.

Key takeaways

  • Tools are actions. Resources are readable data. Prompts are insertable templates.
  • Split catalogs into resources and precise queries into tools.
  • Keep the tool surface small, named clearly and paginated.

Further learning

Next, see how the host actually talks to the server in stdio and HTTP transports.