Building an MCP server
To build an MCP server you implement the Model Context Protocol — an open JSON-RPC 2.0 standard introduced by Anthropic — so an AI host can perform a capability handshake and then discover and call your tools, resources and prompts. In practice you pick a transport (stdio for local, Streamable HTTP for remote), define each tool with a precise name, description and JSON Schema, and return well-formed results. Building it securely means keeping secrets out of schemas, gating destructive tools, treating tool output as untrusted, and avoiding the lethal trifecta — then re-auditing on every release.
What you are actually implementing
An MCP server is a process that speaks the Model Context Protocol — JSON-RPC 2.0 messages — to a host's MCP client. The host opens one MCP client per server, performs a capability handshake (exchanging protocol version and the features each side supports), then calls tools/list, resources/list and prompts/list to discover what you expose, and tools/call to invoke a tool. You do not have to implement this wire format by hand: official SDKs (Python, TypeScript and others) and higher-level frameworks like FastMCP handle the protocol, leaving you to declare tools and write their logic.
The three primitives you can expose are tools (callable functions, each with a name, description and JSON Schema for inputs and, optionally, outputs), resources (readable data addressed by URI), and prompts (reusable, parameterized templates). Most servers are tool-centric. Every one of those declarations is read straight into the model's context, so the quality and safety of your names, descriptions and schemas is part of the product, not documentation.
Choosing a transport: local vs remote
MCP defines a few transports. stdio runs your server as a local subprocess that the host launches and talks to over standard input/output — the simplest option for a desktop tool or a server distributed as a package, with no network surface. Streamable HTTP is the current remote transport for servers reachable over the network; the older HTTP+SSE pairing is its legacy predecessor and is still seen in the wild.
Pick stdio when the server runs on the same machine as the host and needs no remote access — it sidesteps a whole class of network exposure. Pick Streamable HTTP when the server must be hosted and shared. Remote servers should sit behind proper authentication (commonly OAuth 2.1 or a Bearer token) and TLS, because anything reachable over the network is reachable by attackers too. Whichever you choose, the protocol surface the host sees is the same — only the connection differs.
Building with an SDK or FastMCP
The fastest path is a maintained SDK. With the Python SDK's FastMCP API you declare a tool as an ordinary function with type hints and a docstring; the framework derives the JSON Schema from your signature, registers the tool, and serves it over stdio or HTTP in a couple of lines. The TypeScript SDK offers the same ergonomics. Using an SDK means you inherit a correct handshake, well-formed JSON-RPC 2.0 errors, and protocol-version negotiation for free — three things hand-rolled servers routinely get wrong.
Spend your effort on the parts the framework cannot decide for you: write tool descriptions that are precise but lean (they cost tokens on every request), give each parameter a clear schema with sensible types and enums, set annotations like readOnlyHint on read-only tools and destructiveHint on tools that mutate or delete, and return structured, predictable output. Keep the tool count focused — consolidate overlapping actions rather than shipping dozens of near-duplicate tools — because sprawl bloats context and makes the model pick the wrong tool.
Writing a secure MCP server
Treat your server as something an agent will trust twice: it reads your tool definitions as context and your tool outputs as data, and both channels can carry text a model may follow as instructions. Never hardcode secrets, API keys, tokens or credentials into a tool's description, default values or examples — they get shipped to the client verbatim and are one of the fastest ways to fail a security audit. Load secrets from the environment or a secrets manager at runtime instead.
Gate anything consequential. Tools that delete, send, post, pay or overwrite should be clearly marked (destructiveHint) and ideally require explicit confirmation rather than firing on a single model decision. Be deliberate about the lethal trifecta: if one server can ingest untrusted external content (fetch, scrape, web-search), reach sensitive data (files, mailboxes, databases, secrets), and send data out or take destructive action, a single injected instruction can become a real breach. Break at least one leg — separate untrusted-content tools from secrets-bearing tools, or remove the unguarded outbound path.
Finally, assume tool output is untrusted. If a tool relays a web page, an email or a comment an attacker wrote, that text can carry agent-directed instructions (tool-response poisoning) even when your code is clean. Validate and constrain inputs, avoid blindly fetching caller-supplied URLs (an SSRF and exfiltration vector), and re-audit your server on every release so a refactor doesn't quietly introduce one of these issues.
How CheckMCP handles it
CheckMCP is the verification step for a server you have just built. Point it at your live endpoint (uvx checkmcp <url>, the web app at checkmcp.dev, or the GitHub Action uses: H129hj/checkmcp@v1 in CI) and it produces an explainable, vendor-neutral MCP Score /100 (grade A–F) across seven weighted pillars — security (20), tool design (18), schemas and descriptions (16), reliability (14), context-cost (12), compliance (12) and coverage (8) — telling you exactly where your server loses points and why. The security pillar runs an OWASP MCP Top 10 pass against the same mistakes this page warns about: a hardcoded secret found in a tool schema caps your grade at D, a critical injected instruction (tool poisoning) in a description or schema does too, and a failed MCP handshake caps it at F. Opt-in behavioral evals exercise only your read-only tools with benign canary inputs to catch tool-response poisoning and data exfiltration before they reach an agent, never invoking mutating tools. Wire the GitHub Action into CI to fail the build on a score regression or rug-pull, and run the in-band Gateway (passive observe-and-log or active block-and-strip) to stop tool-poisoning and drift at runtime, with drift monitoring on tracked servers. If your server is distributed as a repo or stdio package, CheckMCP also grades the project on four pillars — maintenance (40), license (25), adoption (20) and documentation (15) — so you ship something both safe and credible.