Tools, Resources, and Prompts
Combining Capabilities
Each capability on its own is straightforward. The design work starts when one server offers all three and you have to decide which job goes where.
A Complete Example: Knowledge Base Server
Here is the same knowledge base exposed three ways at once. Notice that each capability answers a different question about the same documents:
One knowledge base, three surfaces
The pairing that makes this work is search_knowledge and doc://{id}. Resources are addressable but not discoverable by the model — it has no way to guess a document ID. The search tool exists specifically to turn a question into the URIs the resource layer can serve. Ship one without the other and half the server is unreachable.
from mcp.server import MCPServer
mcp = MCPServer(name="knowledge-base")
# 1. RESOURCES — read-only views, addressed by URI
@mcp.resource("doc://{doc_id}", mime_type="text/markdown")
async def document(doc_id: str) -> str:
"""A single knowledge-base document."""
doc = await db.get_document(doc_id)
return doc.content
# 2. TOOLS — actions, including the ones with side effects
@mcp.tool()
async def search_knowledge(query: str, limit: int = 5) -> str:
"""Search the knowledge base and return the best matching documents."""
return await kb.search(query, limit=limit)
@mcp.tool()
async def add_document(title: str, content: str, tags: list[str] | None = None) -> str:
"""Add a new document to the knowledge base."""
doc_id = await db.add_document(title, content, tags or [])
return f"Created doc://{doc_id}"
# 3. PROMPTS — standardised interactions the user picks from a menu
@mcp.prompt()
def summarize_topic(topic: str) -> str:
"""Summarise everything the knowledge base knows about a topic."""
return f"Search the knowledge base for '{topic}', then summarise what you find. Cite each doc:// URI you used."
The three capabilities are doing three different jobs here, and the split is the whole
design. doc://{doc_id} is a template, so the server exposes every document without
listing any of them. add_document is a tool rather than a writable resource because it has
a side effect. summarize_topic is a prompt because the user chooses it deliberately —
prompts are user-initiated, which is exactly what tools are not.
Workflow: AI Using All Capabilities
Trace one real request end to end. The order is not arbitrary — search has to come first, because nothing else knows which documents exist:
"Summarise our AI security policies"
summarize_topic with topic="AI security". Optional — a user who knows what they want can skip straight to asking
Tool call. Returns doc://policy-123 and doc://policy-456. This is the discovery step — without it the model has no IDs to read
Resource read. Full text, no arguments, no side effects
Same again. Reads are cheap and independent, so they can run together
No server involvement. The model works from text it actually retrieved rather than from memory
Steps two and three are the ones worth internalising. Search returns identifiers, not content; the resource layer turns identifiers into content. Collapsing those into a single tool that returns full documents seems tidier until a search matches thirty files and the context window is gone.
Resource + Tool Coordination
Design resources and tools to work together:
# Resource: Read-only view
Resource(uri="user://123", name="User 123 Profile")
# Tool: Modify user
Tool(name="update_user", description="Update user profile")
Best Practices
| Practice | Reason |
|---|---|
| Clear separation | Resources for reading, tools for actions |
| Consistent URIs | Makes resources predictable |
| Related naming | user://123 with update_user tool |
| Document relationships | Explain in descriptions |
Now let's apply these patterns in a hands-on lab. :::
Sign in to rate