ARSTUDIOZ

Hamburger icon

ARSTUDIOZ

X

Say Hi!

hello@arstudioz.com

linkedtwittertelegramupwork

How to Build an MCP Server (Production-Safe)

The short answer

An MCP server exposes tools, resources, and prompts to AI applications over JSON-RPC. The tool logic is usually a few lines. The real work is everything around it: choosing stdio for local versus Streamable HTTP for remote, doing OAuth 2.1 correctly for remote servers, validating every tool argument, and treating tool descriptions as untrusted, because the model reads them and an attacker can hide instructions there. Build the tool in an afternoon, then spend the real time on auth and validation.

Here is the thing nobody tells you before you build your first MCP server: the tool is the easy part. You will write the actual function, the thing that queries your database or sends the message, in about twenty minutes. Then you will spend three days on authentication, input validation, and making sure your server cannot be turned into a data-exfiltration device. That ratio is the whole story, so let us spend our time where the work actually is.

The shape, fast

MCP has three roles: a host (the AI app), a client inside it, and the server you build. Your server exposes three kinds of things over JSON-RPC:

  • Tools the model can call (an action, with a name, a description, and an input schema).
  • Resources it can read (URI-addressable context).
  • Prompts a user can invoke (templates, often surfaced as slash commands).

For a fuller tour of the protocol itself, see what MCP is. Here we are building.

A minimal server

In TypeScript, using the official SDK's McpServer, a working tool is genuinely this small:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "weather", version: "1.0.0" });

server.registerTool(
  "get_forecast",
  {
    description: "Get the forecast for a city.",
    inputSchema: { city: z.string() },
  },
  async ({ city }) => ({
    content: [{ type: "text", text: `Sunny in ${city}.` }],
  })
);

await server.connect(new StdioServerTransport());

Python is just as compact with the high-level FastMCP API, where your type hints become the schema and the docstring becomes the description. One note on versions: as of mid-2026 the stable, production line is the v1 SDKs, targeting the 2025-11-25 spec. Newer major versions exist but are still beta, so do not build production on them yet, and verify class names against the live docs before you copy anything.

That z.string() is not decoration. Validating every argument against a schema is your first and cheapest line of defense.

Local or remote: pick your transport

There are two transports, and the choice shapes everything downstream.

  • stdio is for local servers. The host launches your server as a subprocess and talks over standard input and output. Credentials come from environment variables. There is no OAuth here at all, so builders who bolt authentication onto a local server are usually solving a problem they do not have. The real local risk is command execution, so sandbox what you spawn.
  • Streamable HTTP is for remote servers behind a URL. It replaced the older two-endpoint HTTP-plus-SSE design, which is now deprecated, with a single endpoint that is far friendlier to load balancers.

One high-leverage detail: sessions are optional. If your server keeps no session state, every request is self-contained, which means it scales trivially on serverless and behind an ordinary load balancer with no sticky routing. Reach for stateful sessions only when you truly need the server to push streams, not by default.

The part that is actually hard: auth

For remote servers, your server is an OAuth 2.1 resource server, and the authorization server is a separate role (your own, or Auth0, Okta, Entra). The spec's 2025 revision made that separation explicit, and it hands your server a short list of non-negotiables:

  • Advertise your protected-resource metadata, and answer an unauthenticated request with a 401 that tells the client where to authenticate.
  • Clients use PKCE and request a token bound to your server's audience.
  • Your server must validate that every token was issued specifically for it, and reject anything else.
  • If your server calls an upstream API, it gets its own token. Never pass the client's token through. Token passthrough is the single most common footgun, and the spec forbids it outright.

If that feels like more work than the tool, good, you are paying attention. Most of the MUST-level burden in the spec is auth and validation, not tool logic.

The security bit that surprises everyone

Beyond that, the production checklist is boring and essential: validate every tool argument against its schema, give each tool the least privilege it needs (a tool that cannot delete cannot be tricked into deleting), validate the Origin header on HTTP connections to block DNS-rebinding, and do not trust tool output either, since it can carry injected content back into the model. The same least-privilege discipline runs through all of AI agent security.

Test it, then ship it

Before you connect a server to a real model, run it through the official MCP Inspector, which gives you a UI and a CLI to list and call tools directly. It has a scriptable mode, so you can put tools/list and a few tool calls into CI and catch a broken schema before a model does.

For deployment, local servers ship as a binary the host spawns; remote servers run as a hosted service, and several platforms now offer one-click remote MCP hosting with OAuth handled for you. Those offerings move fast, so check the current feature set before you commit to one.

The takeaway

Building an MCP server is a study in where the real cost lives. The tool is trivial. The value, and the risk, is in the auth you get right, the inputs you validate, and the descriptions you treat as hostile. Get those three right and you have a server you can actually put in front of a model in production.

This is exactly the work we do when we build MCP integrations: the connector is the easy part, and the auth, scoping, and validation are the job. If you are exposing your systems to AI, let us make sure it is done safely.

References

Frequently asked questions

An MCP server is a program that exposes capabilities to AI applications through the Model Context Protocol: tools the model can call, resources it can read, and prompts a user can invoke. It speaks JSON-RPC to a client running inside a host application, and it can run locally as a subprocess or remotely behind a URL.

The two flagship official SDKs are TypeScript and Python, and there are official SDKs for several other languages. In TypeScript you use the McpServer class; in Python the high-level FastMCP API. Stick to the stable v1 line for production, since newer major versions are still in beta as of mid-2026.

A remote MCP server acts as an OAuth 2.1 resource server. It advertises its protected-resource metadata, and the client discovers a separate authorization server, uses PKCE, and obtains a token bound to your server's audience. Your server must validate that every token was issued specifically for it, and must never pass the client's token through to an upstream API. Local stdio servers skip all of this and read credentials from environment variables.

The tool description. The model reads it, so a malicious server can hide instructions in a description that tell the model to leak data while returning a normal-looking result. This is called tool poisoning. Treat any server you consume as untrusted, treat your own descriptions as a security-reviewed artifact, validate all tool inputs, and give each tool the least privilege it needs.

Building something with AI agents?

We design and ship production AI agents for teams in fintech, crypto, and beyond.

Book a call

Keep reading

AI Agents
Multi-Agent Systems: When You Actually Need More Than One
5 min read
AI Agents
Agentic AI Security: The 2026 Threat Landscape
5 min read
AI Agents
How to Evaluate AI Agents (and Not Get Burned)
5 min read