Model Context Protocol (MCP) · 18 min read

Setting Up an MCP Server

Plan and implement a small MCP server that exposes safe, well-described capabilities.

MCP host, client, and server components connected through the protocol. Source: Wikimedia Commons · MCP developers · MIT License

Choose one boundary

A first MCP server should have a narrow purpose and a clear owner. For example, expose read-only project documentation rather than an entire operating system or database. List the users, data, operations, and failure modes inside that boundary. Decide which capabilities are tools, resources, or prompts, then remove anything that is not needed for the first workflow. A small surface reduces the number of permissions, schemas, tests, and logs you must maintain. It also makes it easier to explain to a user exactly what the server can and cannot do.

Set up the runtime

Create a dedicated project with a pinned runtime and the official MCP SDK for your language. Keep configuration outside source code and distinguish development, staging, and production endpoints. The server should start predictably, report a safe health state, and exit with a useful error when required configuration is missing. Use a dedicated operating-system identity or container where practical. Do not run a local experiment with broad personal permissions and then assume the same code is safe when connected to an agent.

Validated MCP tool schemapython
from pydantic import BaseModel, Field

class InvoiceStatusInput(BaseModel):
    invoice_id: str = Field(min_length=1, max_length=40)

def get_invoice_status(arguments: dict) -> dict:
    data = InvoiceStatusInput.model_validate(arguments)
    return {"invoice_id": data.invoice_id, "status": "paid"}

Design tool schemas

A tool schema is an executable contract, so define argument types, required fields, allowed values, length limits, and descriptions that explain observable behavior. Prefer specific operations over arbitrary code, shell, or query execution. Normalize inputs before business logic sees them, reject unknown or ambiguous values, and define whether an operation is read-only or mutating. The server must validate again even if the client already did so. Client validation improves usability; server validation is the actual security boundary.

Implement structured results

Return results that a host can process without scraping prose. Include stable fields, identifiers, timestamps, status values, and source references where they help the user verify an answer. Keep payloads bounded and paginate or summarize large datasets. Distinguish an empty successful result from an execution error. Avoid returning credentials, internal stack traces, hidden prompts, or unrestricted file contents. If a result contains user-controlled text, mark it as data in the host’s rendering and model prompt path.

Add authorization

Authentication answers who connected; authorization answers what that identity may do. Map each tool to an explicit permission and enforce that permission inside the server for every call. Check tenant, object ownership, record scope, and approval requirements close to the operation. Do not rely on a model instruction, a tool description, or a client-side checkbox as authorization. For high-impact mutations, require a server-verifiable approval token or a separate confirmation flow that binds approval to the exact arguments.

Handle operational failure

Wrap external calls with deadlines, bounded retries, connection pooling, and clear error mapping. Make writes idempotent with an idempotency key when a retry could repeat a side effect. Propagate cancellation to downstream clients and release resources in all exit paths. Log request IDs, tool names, validated argument summaries, authorization decisions, duration, and result status. Redact secrets and sensitive payloads. Metrics should make timeout rate, rejection rate, dependency errors, and unusually large results visible before users report them.

Test the server

Test the server at the protocol boundary, not only through internal functions. Cover startup with missing configuration, discovery, schema validation, authorization failures, expired credentials, path or query injection, timeouts, cancellation, malformed dependencies, duplicate requests, and oversized responses. Use a real compatible client for a smoke test, then run a small adversarial suite against every tool. Document the supported protocol version and deployment assumptions. A server is ready when its safe behavior is repeatable under both normal requests and hostile input.