Back to the map

Module 15 · Protocols and frameworks · Lesson 2 of 5

Building and securing an MCP server

An MCP server holds real credentials and serves a model that can be tricked: few well-described tools, least privilege per tool, OAuth and per-user authorisation, input validation, elicitation before destructive acts, audit logs.

CoreShould-knowMid 12 minPro Updated Nov 2026

Why it matters

An MCP server is a small program that holds real credentials and does whatever a model asks, and a model can be talked into asking for almost anything. A REST API serves a client an engineer wrote; a server serves a client written by whoever last put text in front of the model. A 2026 analysis of public servers found most open to path traversal or code injection, and an OWASP MCP Top 10 now exists in beta. Interviewers ask about it because it shows fastest whether you know where the trust boundary sits.

The idea

Design the surface first

Expose few tools, each well described. The Designing a tool lesson applies: verb-first names, a description that says when and when not, typed arguments with examples, enums for closed sets, absolute references. For a server that reads files and runs reports that means read_file(path), list_dir(path), run_report(report_id, period) and nothing broader. Put reference data in resources: the list of available reports, the directory layout. Put repeatable workflows in prompts: "monthly close" as a template naming the reports and their order. Return structured results, a few typed fields and an id rather than a page of text, and errors as data the model can act on.

The security model

The server runs with its own credentials, so the server process is the credential boundary; the client never holds the secret. Six controls do most of the work.

Least privilege per tool. Each tool's credential should allow only what that tool does: read_file under a role that reads one directory tree, run_report under one that calls the reporting service and nothing else. One broad service account shared by every tool turns a single bad call into full access.

Authentication on remote servers. A streamable HTTP server is a network service. The recommended scheme is OAuth: the host obtains a token for the user and the server validates it on every request. A stdio server inherits the local user's identity.

Authorisation per user, not per server. Knowing the host may connect is not knowing this user may read this file. Check each call against the identity in the token, so a finance lead and an intern get different answers from the same tool.

Input validation. Treat every argument as untrusted; a model acting on injected text may have produced it. Resolve paths and reject any that leave the allowed root; that one check fixes the traversal finding. Parameterise queries and never build shell commands from user-shaped strings; that fixes injection.

Rate limits and audit logs. A model in a loop can make a thousand calls a minute. Limit per user and per tool, and log every call with who, what, which arguments and the outcome, so a breach can be reconstructed and a runaway loop spotted.

Elicitation before destructive actions. For a delete, a payment or a send, use elicitation to ask the user, through the host, for confirmation. The model's intent is not the user's consent.

Clientserver: credential boundaryAuthValidateHandlerAudit logno token: rejectbad path: errorSecretswho, what, whenper-tool scopeclient holds no secrets; the server decides per user and per tool
Every call crosses the credential boundary into the server, where it is authenticated, validated, executed under a per-tool scope and logged. The client never holds a secret.

Versioning and discovery

Each request carries the version and capabilities it was written against, and the server answers server/discover with the versions it supports. Keep that answer honest. Add tools without removing old ones, deprecate by description before you delete, and never change an argument's meaning under the same name, because a host may have cached the definition.

Test it like a product

Run the server under an inspector tool before any host sees it: list the tools, call each with good and bad arguments, read the exact result the model would read. Then run it against a real agent and read the transcripts. Add hostile cases to the test set: traversal paths, injected instructions in file contents, arguments that are valid and wrong. One audit line answers most incident questions later.

2026-11-13T09:14:02Z user=u_41 tool=run_report args_hash=9f2c scope=reports:read status=ok 412ms
 Local stdio serverRemote HTTP serverGateway in front of many servers
AuthInherits the local user; none on the wireOAuth token per user, validated per requestOne OAuth front door; it maps identity to each backend
ExposureThe user's machine onlyA network service; every control above appliesOne hardened surface; backends stay private
OperationsInstalled per machine; versions driftDeployed once; monitored like any serviceCentral rate limits, logs and policy; a single point of failure
FitDeveloper tools, personal data, anything with no server to runShared company systems used from many hostsDozens of servers, several teams, compliance needs
Building and securing an MCP server · Backplane