MCP Server

Intermediate4 min

Tools are model-controlled, resources are application-controlled. That column decides which primitive a capability belongs to.

#mcp
#security

Where the server sits among MCP roles

Three roles. The host is the application a person uses, such as an editor or a chat client. The client lives inside the host and holds one connection. The server is a separate process that exposes capabilities over that connection.

The server is where your code goes. It decides what a model can reach, which makes it a boundary before it is a feature.

The three primitives, and who controls each

The specification defines what a server exposes, and it comes with a column most introductions leave out.

PrimitiveControlWhat it isExample
PromptsUser-controlledTemplates the user invokesSlash commands, menu options
ResourcesApplication-controlledContextual data the client attachesFile contents, git history
ToolsModel-controlledFunctions the model can call to actAPI requests, file writing

Read down the control column first. It answers the question that matters when you design a server: who decides this happens.

A prompt runs because a person chose it. A resource is read because the host decided to attach it. A tool runs because the model asked for it.

Choosing a primitive for a capability

Suppose you are exposing a company handbook.

As a resource, the host decides when to attach it. A user opens a support conversation, the application attaches the relevant section, and the model works from what it was given.

As a tool, search_handbook becomes something the model invokes when it judges the handbook relevant. That is more flexible and it moves the decision to the model.

Neither is wrong. The question is whether you want the application deciding or the model deciding, and that is a design choice rather than a technical one.

The rule of thumb: if the host knows when the data is needed, make it a resource. If knowing requires reading the conversation, make it a tool. Anything that changes state should be a tool, because tools are the primitive with a permission story attached.

The server as a capability boundary

A server exposes a fixed set of capabilities. Whatever is not exposed cannot be reached, whatever the model asks for.

That makes the server the right place to enforce scope. A server over a support database that exposes lookup_ticket and nothing else cannot be talked into deleting a ticket, because there is no such capability to invoke. Narrowing what you expose is more reliable than instructing a model not to use something.

Inside a tool handler, the same rules apply as anywhere else arguments arrive from outside. Validate them, check permissions against the acting user rather than the model's request, and treat the whole payload as untrusted. The reasoning is the same as in function calling, because from your handler's point of view this is the same situation.

Why server output is untrusted input to the model

The direction people forget runs the other way.

Whatever your server returns lands in the model's context, where it is indistinguishable from your own instructions. A tool that returns the text of a document returns whatever that document says, including any instruction somebody wrote into it.

So a compromised server, or an honest server over attacker-controlled content, is an injection vector. The MCP client trusts the server it connected to, and the model reads what arrives.

Two consequences for anyone running a host. Servers are a supply chain, so installing one is a trust decision equivalent to installing a dependency that can write to your context. And the decisions that matter belong in code that runs after the model responds, not in a prompt asking the model to ignore instructions it finds in tool results. Prompt injection covers the attack in general; this is the MCP-shaped version of it.

Further reading

Knowledge check

Question 1 of 3

You expose a `delete_ticket` capability on an MCP server. Which primitive should it be, and why?

Sign in to save your progress and pick up where you left off.