MCP servers expose three capability types: Tools are model-controlled actions the AI invokes autonomously (like running a query), Resources are application-controlled data the host loads for context (like file contents), and Prompts are user-controlled templates that structure AI interactions (like a code review checklist). Choosing the right capability type determines who controls the interaction and how the data flows between user, model, and server.
Three Capability Types, Three Control Models
The single most important design decision when building an MCP server is choosing the right capability type for each piece of functionality. Tools are invoked by the AI model during reasoning — the model decides when to call them. Resources are loaded by the host application to provide context — the app decides when to fetch them. Prompts are selected by the user to guide interactions — the user decides when to use them. Getting this wrong leads to poor UX: a resource that should be a tool means the AI cannot act autonomously, and a tool that should be a resource means unnecessary model calls for static data.
Prerequisites
- Understanding of MCP's host-client-server architecture
- Basic TypeScript or Python knowledge for code examples
- An MCP server project initialized (or willingness to read code conceptually)
Step-by-step guide
Understand Tools — model-controlled actions
Understand Tools — model-controlled actions
Tools are the most common MCP capability. They represent actions the AI model can invoke during its reasoning process. The model sees tool descriptions and schemas, decides when calling a tool would help answer the user's question, and sends a tool call request. Examples: querying a database, creating a GitHub issue, sending a Slack message, running a calculation. Tools are like giving the AI a set of functions it can call. The key characteristic is that the model controls when and how tools are invoked — the user does not explicitly select them.
1// TypeScript: defining a tool2import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";3import { z } from "zod";45const server = new McpServer({ name: "demo", version: "1.0.0" });67server.tool(8 "search_issues",9 "Search GitHub issues by keyword",10 {11 query: z.string().describe("Search keywords"),12 repo: z.string().describe("Repository in owner/repo format"),13 state: z.enum(["open", "closed", "all"]).default("open"),14 },15 async ({ query, repo, state }) => {16 // Call GitHub API...17 return {18 content: [{ type: "text", text: `Found 12 issues matching "${query}" in ${repo}` }],19 };20 }21);Expected result: You understand that tools are model-invoked actions with input schemas, and the AI autonomously decides when to call them.
Understand Resources — application-controlled data
Understand Resources — application-controlled data
Resources represent data that the host application can load to give the AI additional context. Unlike tools, resources are not called by the model during reasoning. Instead, the host application (or user through the host's UI) explicitly loads resources before or during a conversation. Resources have URIs and can return text or binary data. Examples: the contents of a configuration file, documentation pages, a user's profile data, system status. Think of resources as read-only data sources that provide background context.
1// TypeScript: defining a static resource2server.resource(3 "project-config",4 "config://project",5 { description: "Current project configuration" },6 async (uri) => ({7 contents: [{8 uri: uri.href,9 mimeType: "application/json",10 text: JSON.stringify({ name: "my-app", version: "2.1.0", env: "production" }),11 }],12 })13);1415// TypeScript: defining a resource template (dynamic URI)16import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";1718server.resource(19 "user-profile",20 new ResourceTemplate("users://{userId}/profile", { list: undefined }),21 { description: "User profile by ID" },22 async (uri, { userId }) => ({23 contents: [{24 uri: uri.href,25 mimeType: "application/json",26 text: JSON.stringify({ id: userId, name: "Jane Doe", role: "admin" }),27 }],28 })29);Expected result: You understand that resources are application-loaded data sources identified by URIs, used to provide context rather than perform actions.
Understand Prompts — user-controlled templates
Understand Prompts — user-controlled templates
Prompts are reusable message templates that users explicitly select to structure AI interactions. They appear in the host's UI (e.g., as slash commands or menu options) and generate pre-formatted messages. Prompts can accept parameters from the user. Examples: a code review prompt that takes a file path, a bug report template that takes an error description, a summarization prompt that takes a topic. The key difference is user control — prompts are never invoked automatically by the model.
1// TypeScript: defining a prompt template2server.prompt(3 "code-review",4 "Review code for bugs, security issues, and best practices",5 {6 language: z.string().describe("Programming language"),7 focus: z.enum(["bugs", "security", "performance", "all"]).default("all"),8 },9 ({ language, focus }) => ({10 messages: [11 {12 role: "user",13 content: {14 type: "text",15 text: `Review the following ${language} code. Focus on: ${focus}.1617Check for:18- Logic errors and edge cases19- Security vulnerabilities (injection, XSS, auth issues)20- Performance bottlenecks21- Adherence to ${language} best practices2223Provide specific line-level feedback with severity ratings.`,24 },25 },26 ],27 })28);Expected result: You understand that prompts are user-selected message templates that structure AI interactions, with optional parameters.
Compare control models with a decision framework
Compare control models with a decision framework
Use this decision framework to choose the right capability type. Ask: Who should control when this is used? If the AI model should decide autonomously based on the conversation, use a Tool. If the application should load data for background context, use a Resource. If the user should explicitly choose a structured interaction, use a Prompt. Ask: Does this perform an action or provide data? Actions (write, create, send, execute) are almost always Tools. Static or semi-static data is usually a Resource. Structured conversation starters are Prompts.
1// Decision matrix:2//3// | Question | Tool | Resource | Prompt |4// |-----------------------------------|------|----------|--------|5// | Who controls invocation? | AI | App | User |6// | Does it perform an action? | Yes | No | No |7// | Does it have side effects? | Often| Never | Never |8// | Does it need input schemas? | Yes | No* | Yes |9// | Is it read-only? | No | Yes | Yes |10// | Does the model see it in context? | Via result | Via host | Via message |11//12// * Resource templates can have URI parametersExpected result: You can apply the decision framework to determine the correct capability type for any MCP server feature.
See real-world examples of correct capability choices
See real-world examples of correct capability choices
Here are practical examples showing the right capability for common scenarios. A GitHub server: 'create_issue' is a Tool (model-controlled action), 'repo-readme' is a Resource (app-loaded context), 'bug-report-template' is a Prompt (user-selected structure). A database server: 'run_query' is a Tool, 'table-schema' is a Resource, 'optimize-query' is a Prompt. A documentation server: 'search_docs' is a Tool, 'api-reference' is a Resource, 'explain-concept' is a Prompt. The pattern is consistent: actions are Tools, data is Resources, templates are Prompts.
Expected result: You can identify the correct capability type for real-world MCP server features across different domains.
Implement all three capabilities in Python with FastMCP
Implement all three capabilities in Python with FastMCP
The Python SDK uses decorators to define capabilities. The @mcp.tool() decorator creates tools, @mcp.resource() creates resources, and @mcp.prompt() creates prompts. The pattern mirrors TypeScript but uses Python type hints instead of Zod schemas. For teams needing help designing capability architectures for complex MCP servers, RapidDev offers consulting on MCP server design patterns.
1# Python: all three capability types with FastMCP2from mcp.server.fastmcp import FastMCP34mcp = FastMCP("demo-server")56# TOOL — model-controlled action7@mcp.tool()8async def search_issues(query: str, repo: str, state: str = "open") -> str:9 """Search GitHub issues by keyword."""10 return f'Found 12 issues matching "{query}" in {repo}'1112# RESOURCE — app-controlled data13@mcp.resource("config://project")14async def get_project_config() -> str:15 """Current project configuration."""16 return '{"name": "my-app", "version": "2.1.0"}'1718# PROMPT — user-controlled template19@mcp.prompt()20async def code_review(language: str, focus: str = "all") -> str:21 """Review code for bugs, security, and best practices."""22 return f"Review the following {language} code. Focus: {focus}."Expected result: You can define all three capability types in both TypeScript and Python MCP servers.
Complete working example
1// Complete MCP server demonstrating all three capability types2// Run: npx -y tsx capabilities-demo-server.ts34import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";5import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";6import { z } from "zod";78const server = new McpServer({9 name: "capabilities-demo",10 version: "1.0.0",11});1213// === TOOLS (model-controlled) ===1415server.tool(16 "add_todo",17 "Add a new todo item",18 {19 title: z.string().describe("Todo title"),20 priority: z.enum(["low", "medium", "high"]).default("medium"),21 },22 async ({ title, priority }) => {23 const id = Math.random().toString(36).slice(2, 8);24 return {25 content: [{ type: "text", text: `Created todo ${id}: "${title}" (${priority})` }],26 };27 }28);2930server.tool(31 "list_todos",32 "List all todo items",33 {},34 async () => ({35 content: [{ type: "text", text: "1. Fix login bug (high)\n2. Update docs (low)" }],36 })37);3839// === RESOURCES (app-controlled) ===4041server.resource(42 "app-status",43 "status://app",44 { description: "Current application status" },45 async (uri) => ({46 contents: [{47 uri: uri.href,48 mimeType: "application/json",49 text: JSON.stringify({50 uptime: "4h 32m",51 todos: 2,52 lastSync: new Date().toISOString(),53 }),54 }],55 })56);5758server.resource(59 "todo-by-id",60 new ResourceTemplate("todo://{todoId}", { list: undefined }),61 { description: "Get a specific todo by ID" },62 async (uri, { todoId }) => ({63 contents: [{64 uri: uri.href,65 mimeType: "application/json",66 text: JSON.stringify({ id: todoId, title: "Fix login bug", priority: "high" }),67 }],68 })69);7071// === PROMPTS (user-controlled) ===7273server.prompt(74 "daily-standup",75 "Generate a daily standup summary",76 {77 team: z.string().describe("Team name"),78 },79 ({ team }) => ({80 messages: [{81 role: "user",82 content: {83 type: "text",84 text: `Generate a daily standup summary for the ${team} team. Include: what was done yesterday, what is planned today, and any blockers. Format as bullet points.`,85 },86 }],87 })88);8990const transport = new StdioServerTransport();91await server.connect(transport);92console.error("Capabilities demo server running");Common mistakes
Why it's a problem: Making everything a tool
How to avoid: Static data (config files, schemas, documentation) should be resources, not tools. Using tools for read-only data wastes model reasoning tokens and adds unnecessary latency. Reserve tools for actions with side effects or dynamic queries.
Why it's a problem: Using resources for actions that have side effects
How to avoid: Resources must be read-only and side-effect-free. If reading the resource creates a record, sends a notification, or modifies state, it should be a tool instead. Resources are for data retrieval only.
Why it's a problem: Forgetting that prompts need user interaction
How to avoid: Prompts are not automatically used by the model. They require the user to explicitly select them in the host's UI. If you want the AI to autonomously use a template, encode that logic in a tool's implementation instead.
Why it's a problem: Not providing descriptions for capabilities
How to avoid: The AI model uses tool descriptions to decide when to call them. Vague descriptions like 'does stuff' lead to incorrect tool selection. Write specific descriptions: 'Search GitHub issues by keyword and return titles, URLs, and labels.'
Best practices
- Use tools for actions (create, update, delete, search, execute) that the AI should invoke autonomously
- Use resources for static or semi-static data that provides background context to the conversation
- Use prompts for structured interaction templates that users explicitly select
- Write detailed tool descriptions — the model uses them to decide when to call each tool
- Keep tool input schemas minimal — only require parameters the tool actually needs
- Use Zod .describe() on every parameter so the model knows what values to provide
- Resources should be idempotent and side-effect-free — reading a resource must never modify state
- Test all three capability types with MCP Inspector before connecting to a host
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Explain the three MCP capability types: tools, resources, and prompts. For each, explain the control model (who invokes it), give three real-world examples, and show the TypeScript code to define one. Then create a decision framework for choosing between them.
I am building an MCP server for a project management system. I need to: (1) create tasks, (2) read project metadata, (3) generate sprint planning prompts. Help me decide which MCP capability type to use for each and show the TypeScript implementation.
Frequently asked questions
Can a single MCP server expose tools, resources, and prompts at the same time?
Yes. A server can expose any combination of the three capability types. During the initialization handshake, the server declares which capabilities it supports, and the host enables the corresponding features.
Do all hosts support all three capability types?
Not all hosts support prompts yet. Tools and resources are widely supported across Claude Desktop, Cursor, VS Code, and Windsurf. Prompt support varies — Claude Desktop supports prompts as slash commands, but some hosts do not expose prompts in their UI.
Can the AI model call resources directly?
No. Resources are application-controlled, meaning the host or user loads them. The model cannot autonomously request a resource the way it can call a tool. If you need the model to dynamically fetch data, use a tool that returns the data.
How do resource templates differ from regular resources?
Regular resources have a fixed URI (like 'config://app'). Resource templates have parameterized URIs (like 'users://{userId}/profile') that accept dynamic values. Templates let one resource definition serve many different data items.
Should I use a tool or a resource for database schema information?
Use a resource. Database schema is relatively static metadata that provides context. The model does not need to 'action' anything to read a schema. Expose it as a resource so the host can load it as background context for database-related conversations.
Can prompts include dynamic data from the server?
Prompts can include embedded resources in their messages using resource references. This lets a prompt template include dynamic server data. However, the prompt itself is still user-triggered — the dynamic data is fetched when the user selects the prompt.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation