Model Context Protocol servers expose tools an assistant can call. The security question is what happens when the model is persuaded to call one it shouldn't, and given prompt injection has no reliable fix, that has to be assumed rather than prevented.
Tool arguments are user input
The model constructs arguments, and the model can be influenced by anything it has read. So arguments arriving at your tool are untrusted, exactly like an HTTP request body.
Validate against a schema, reject rather than coerce, and apply the same protections you would to any endpoint. Parameterised queries, path resolution against a base directory, allowlists for hosts.
A read_file tool taking a path is a path traversal waiting to happen if you skip this.
Scope credentials to the user, not the server
The common shortcut is a service account with broad access, because it makes everything work. That means a successful injection reaches everything the service account can reach.
Pass through the calling user's identity and authorise against it. If the model can only touch what the user could touch anyway, an injection doesn't cross a privilege boundary, it just does something the user could have done.
Separate read from write
Read-only tools are dramatically lower risk. Split them, and make destructive operations opt-in rather than present by default.
For anything irreversible. Deleting, sending, deploying, paying, require explicit human confirmation of the specific action. General approval of the agent isn't the same thing, and this is the single most effective control available.
Tool descriptions are part of the prompt
Descriptions and schemas get injected into the model's context, which makes them an injection surface in their own right. A malicious or compromised third-party server can describe its tools in ways that influence how the model uses other servers' tools.
If you're assembling tools from multiple sources, that's a supply chain. Review descriptions the way you'd review a dependency, pin versions, and be cautious about servers you don't control.
Return data, not instructions
Tool output goes back into the model's context. If your tool returns content from an untrusted source. A fetched page, a user-submitted ticket, that content can carry an injection.
You can't sanitise this reliably. What you can do is keep the tool's authority low enough that it doesn't matter, and avoid pairing a tool that reads untrusted content with tools that have write access in the same session.
Transport and auth
Local stdio servers inherit the permissions of the process. Remote servers need real authentication, per-user tokens, TLS, and rate limits.
Log every call with the arguments, the caller and the outcome. You need this to investigate, and there won't be an obvious signature at the time.
The framing
An MCP server is an API whose client can be talked into things. Every control that matters is on your side of the boundary.