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 Server
from mcp.types import Tool, Resource, Prompt, TextContent
server = Server(name="knowledge-base")
# 1. RESOURCES: Expose documents
@server.list_resources()
async def list_resources():
docs = await db.get_all_documents()
return [
Resource(
uri=f"doc://{doc.id}",
name=doc.title,
description=f"Document: {doc.title}",
mimeType="text/markdown"
)
for doc in docs
]
@server.read_resource()
async def read_resource(uri: str):
doc_id = uri.replace("doc://", "")
doc = await db.get_document(doc_id)
return [TextContent(type="text", text=doc.content)]
# 2. TOOLS: Search and modify
@server.list_tools()
async def list_tools():
return [
Tool(
name="search_knowledge",
description="Search the knowledge base",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string"},
"limit": {"type": "integer", "default": 5}
},
"required": ["query"]
}
),
Tool(
name="add_document",
description="Add a new document to the knowledge base",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}}
},
"required": ["title", "content"]
}
)
]
# 3. PROMPTS: Standardized interactions
@server.list_prompts()
async def list_prompts():
return [
Prompt(
name="summarize_topic",
description="Summarize all documents on a topic",
arguments=[
PromptArgument(name="topic", required=True)
]
)
]
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