MCP Tools vs Resources vs Prompts: When to Use Which Primitive
Most MCP servers expose everything as tools. The protocol has three primitives for a reason, and here's when each one is the right choice, with Go examples from servers I run in production.
Most MCP servers expose everything as tools. That’s what the tutorials teach: define a function, register it, done. And it works. But the protocol has three distinct primitives, and flattening everything into tools creates real friction. Agents end up calling tools when they should be reading resources. They miss prompt templates that would improve their output quality. The server does more work than it needs to, and the agent’s context window fills up with unnecessary function calls.
I’ve been running four MCP servers in production: scry for code intelligence, tome for database schemas, lore for git history, and flume for HTTP inspection. Each one uses all three primitives, and the distinction between them matters more than I expected when I started building.
This is the breakdown I wish someone had written when I was figuring out which primitive to use where.
Tools: functions the agent calls
Tools are the most familiar primitive. They’re functions with typed parameters that the agent invokes to perform operations. The agent decides when to call them, passes arguments, and gets structured results back.
Tools are the right choice when:
- The operation requires parameters (a query, a file path, a symbol name)
- There are side effects (writing data, triggering a build)
- The result depends on dynamic input the agent provides at call time
In my servers, scry_defs is a tool because the agent needs to pass a symbol name. tome_describe is a tool because it takes a table name. lore_blame is a tool because it needs a file path and optional line range. All of these require the agent to decide what to look up.
Here’s how tool registration looks with the official Go SDK:
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type DescribeInput struct {
Table string `json:"table" jsonschema:"description=the table name to describe"`
}
func Describe(ctx context.Context, req *mcp.CallToolRequest, input DescribeInput) (
*mcp.CallToolResult, any, error,
) {
schema, err := getTableSchema(ctx, input.Table)
if err != nil {
return nil, nil, err
}
return &mcp.CallToolResult{
Content: []mcp.Content{{Type: "text", Text: schema}},
}, nil, nil
}
func main() {
server := mcp.NewServer(
&mcp.Implementation{Name: "tome", Version: "v1.0.0"}, nil,
)
mcp.AddTool(server, &mcp.Tool{
Name: "tome_describe",
Description: "Describe a database table's columns, types, and constraints",
}, Describe)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
The SDK handles JSON Schema generation from the input struct’s tags, parameter validation, and marshaling. You write a function, the framework does the rest.
Resources: data the agent reads
Resources are URI-addressed, read-only data. The agent doesn’t call them with parameters. Instead it reads them by URI, like fetching a file. Resources are for information the agent should have, not operations it should perform.
Resources are the right choice when:
- The data doesn’t require parameters to produce
- It’s reference context that helps the agent reason better
- The content is relatively stable (not changing per-request)
- You want the data available without the agent spending a tool call
The key insight: if an agent would always call your tool with the same arguments, that tool should be a resource. A full database schema dump doesn’t need parameters. A project’s dependency list doesn’t need parameters. The list of available tables doesn’t need parameters. These are all resources.
server.AddResource(
&mcp.Resource{
Name: "schema",
URI: "tome://schema/full",
Description: "Complete database schema including all tables, columns, and relationships",
MIMEType: "application/json",
},
func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
schema, err := dumpFullSchema(ctx)
if err != nil {
return nil, err
}
return &mcp.ReadResourceResult{
Contents: []mcp.ResourceContents{{
URI: "tome://schema/full",
MIMEType: "application/json",
Text: schema,
}},
}, nil
},
)
Resources show up in capability discovery. A well-behaved AI client will read relevant resources proactively to build context before it starts working. This means the agent has your schema in its context window before it decides which table to query, leading to better tool calls downstream.
Prompts: reusable templates
Prompts are the least used primitive and, in my opinion, the most interesting. A prompt is a template that the agent (or user) can select to get back a structured set of messages: essentially a pre-built conversation starter that shapes how the agent approaches a task.
Prompts are the right choice when:
- You want to guide the agent’s reasoning approach for a specific workflow
- A task requires multi-step instructions that the agent shouldn’t have to invent
- You want consistent output formatting across invocations
- The agent needs domain-specific guidance to produce good results
Think of prompts as recipes. The agent picks one, gets back a system message and maybe some user messages, and proceeds with that framing. It’s like handing someone a checklist before they start work.
server.AddPrompt(
&mcp.Prompt{
Name: "analyze_indexes",
Description: "Analyze a table's indexing strategy and suggest improvements",
Arguments: []mcp.PromptArgument{{
Name: "table",
Description: "The table to analyze",
Required: true,
}},
},
func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
table := req.Params.Arguments["table"]
return &mcp.GetPromptResult{
Messages: []mcp.PromptMessage{
{
Role: "user",
Content: mcp.TextContent{
Type: "text",
Text: "Analyze the indexing strategy for the " + table + " table. " +
"First read the full schema resource, then use tome_describe to get " +
"column details. Check for: missing indexes on foreign keys, " +
"redundant composite indexes, columns frequently used in WHERE " +
"clauses that lack indexes. Suggest specific CREATE INDEX statements.",
},
},
},
}, nil
},
)
The prompt doesn’t do the work. It tells the agent how to do the work: which resources to read first, which tools to call, what to look for, what format to produce. That’s orchestration logic that lives in the server, not in the user’s head.
Decision framework
When you’re decomposing a server’s capabilities, ask these questions in order:
Does the agent need to pass parameters? It’s a tool. The agent is making a decision about what to operate on, so it needs to call a function with arguments.
Does the agent need reference context? It’s a resource. The data exists independent of any specific query, and having it available makes the agent’s other work better.
Do you want to shape the agent’s approach to a task? It’s a prompt. You’re encoding workflow knowledge (the steps, the order, the criteria) into a reusable template.
These aren’t mutually exclusive. A good server uses all three together.
Real example: decomposing a database server
Here’s how I think about tome’s capabilities across all three primitives:
Tools are operations that need parameters:
tome_describe: takes a table name, returns its schematome_search: takes a query string, finds matching tables/columnstome_relations: takes a table name, returns foreign key relationshipstome_enums: takes an enum type name, returns its values
Resources are context the agent should have:
tome://schema/full: the complete schema dump (all tables, all columns)tome://schema/tables: just the table names and their descriptionstome://schema/stats: row counts, index sizes, table sizes
Prompts are workflow templates:
analyze_indexes: guides the agent through an indexing auditmigration_review: tells the agent how to evaluate a proposed schema changenormalize_check: walks through normalization analysis for a specific table
The agent reads the schema resource to understand what’s available, uses tools to drill into specifics, and selects prompts when it needs structured guidance on complex analysis tasks. Each primitive serves a different interaction pattern.
Common mistakes
Exposing everything as tools. This is the default because it’s what tutorials teach. But when every piece of data requires a tool call, the agent burns context and latency on operations that should be instant reads. If your tool takes no parameters, it’s a resource.
Not providing resources when the agent needs context. I see servers that have a list_tables tool but no schema resource. So the agent has to call the tool before it can reason about what’s available. That’s backwards. Give it the resource, and it can reason first, then call tools with informed arguments.
Making tools too broad. A single query tool that takes arbitrary SQL is technically functional but gives the agent no guidance. Decompose into specific operations: describe, search, relations. Each tool should do one thing well, with clear parameters that constrain the agent’s choices.
Ignoring prompts entirely. Most servers ship zero prompts. But if you find yourself repeatedly giving an agent the same instructions before it uses your tools effectively, that’s a prompt waiting to be extracted. Put the workflow knowledge in the server where it belongs.
Where this fits
This post covers one piece of the MCP architecture. For the full protocol overview, covering transports, capability negotiation, and the ecosystem, start with What Are MCP Servers?. For a complete implementation walkthrough in Go, covering project structure, BadgerDB caching, and daemon lifecycle, read Building MCP Servers in Go.
The primitives are the conceptual foundation. Once you internalize when each one is appropriate, your servers become easier for agents to use, and you stop fighting the protocol instead of working with it.
Frequently Asked Questions
What is the difference between MCP tools and resources?
Tools are functions the agent calls with parameters, and they perform operations, run queries, or trigger side effects. Resources are read-only data the agent accesses by URI, providing context, reference material, or configuration without requiring parameters. Use tools when the agent needs to do something, resources when it needs to know something.
When should I use MCP resources instead of tools?
Use resources when the data is relatively static, doesn't require parameters to fetch, and serves as background context for the agent. Examples: a database schema dump, a project's configuration, API documentation. If the agent would always call your tool with the same arguments, it should probably be a resource.
What are MCP prompts used for?
Prompts are reusable templates that shape how an agent approaches a task. They provide structured instructions, multi-step workflows, or consistent output formatting. Think of them as starter recipes: the agent selects one and gets back a pre-built conversation that guides its reasoning toward better results.
Can an MCP server have all three primitives?
Yes, and the best servers do. A database MCP server might expose a tool for running queries, a resource for the full schema, and a prompt for analyzing table indexing. Each primitive serves a different interaction pattern, and combining them gives the agent the right interface for each type of work.
How do I register resources and prompts in the Go MCP SDK?
Use server.AddResource() with a Resource struct (containing Name and URI) and a ResourceHandler function, or mcp.AddTool() for tools. For prompts, use server.AddPrompt() with a Prompt struct and a PromptHandler that returns a GetPromptResult containing the template messages.