The Model Context Protocol just went through the biggest architectural change since Anthropic open-sourced it in late 2024. On July 28, 2026, the MCP steering group shipped the 2026-07-28 specification, and it rips out the stateful handshake that made early MCP servers a pain to scale. If you built an MCP server last year, large chunks of that code are now on a deprecation clock. If you’re building one for the first time, you get to skip the mess entirely and start with the stateless model that production teams are standardizing on right now.
This tutorial walks through building an MCP server from an empty folder to a deployed, load-balanced service that both LangGraph 0.4.4 and the OpenAI Agents SDK v0.22.0 can call as a tool. You’ll write real TypeScript, run the official MCP Inspector against it, containerize it, and wire it into two different agent frameworks. By the end you’ll have a complete working project you can adapt for your own tools, plus a troubleshooting section for the errors that trip up almost everyone the first time they build an MCP server.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why the MCP 2026-07-28 Spec Changes How You Build Servers
Before the July update, MCP was a bidirectional, stateful protocol. A client opened a connection, ran an initialize/initialized handshake, got handed a session ID via the Mcp-Session-Id header, and then kept that session pinned to a specific server process for the life of the conversation. That worked fine for a single developer running a local server through a desktop AI app. It fell apart the moment teams tried to run MCP servers behind a normal load balancer, because sticky sessions and shared state stores had to be bolted on for anything resembling horizontal scaling.
The Model Context Protocol project’s own announcement described the shift directly: “With the new spec version, we’ve officially retired the initialize/initialized exchange along with the Mcp-Session-Id header,” according to the official Model Context Protocol blog. Every request now carries its own protocol version, client identity, and capability set inside a _meta field, so no server needs to remember anything about a client between calls.
The MCP roadmap post frames the motivation in operational terms rather than academic ones. The project team explained that removing protocol-level sessions and the initialization handshake means “a server can scale horizontally without holding state,” a line from the official MCP roadmap that effectively rewrites the deployment story for anyone running an MCP server at more than toy scale. WorkOS, which builds authentication infrastructure for agent platforms, put the practical consequence even more bluntly in its analysis of the spec change: “an MCP server can now run behind a plain round-robin load balancer. No sticky routing, no shared session store,” according to WorkOS’s breakdown of the 2026 spec.
Google’s developer blog reached the same conclusion from the infrastructure side, writing that “the new 2026-07-28 specification solves this by making the protocol core completely stateless. The handshake is gone,” per the Google Developers Blog post on scaling agent infrastructure. Three independent sources landing on the same framing is a good signal that this is the direction the ecosystem has actually settled on, not just a proposal still being argued over.
The spec also deprecates three legacy features outright: Roots, Sampling, and Logging. They still work today, and the project has committed to at least a twelve-month support window before pulling them, but new servers should not build on top of them. Legacy HTTP+SSE transport gets the same treatment, with a one-year off-ramp before it’s fully retired in favor of the newer transport mechanisms this tutorial uses.
| Aspect | Pre-2026-07-28 (Stateful) | 2026-07-28 Spec (Stateless) |
|---|---|---|
| Session handling | initialize/initialized handshake, server keeps session state | No handshake, every request self-describes via _meta |
| Session identifier | Mcp-Session-Id header, tied to one server process | Removed entirely |
| Horizontal scaling | Requires sticky routing or a shared session store | Works behind a plain round-robin load balancer |
| Client identity | Established once at handshake time | Sent with every request in the _meta field |
| Roots, Sampling, Logging | Core features | Deprecated, 12-month support window |
| Transport | HTTP+SSE as primary transport | HTTP+SSE deprecated, 1-year off-ramp |
| Tier 1 SDK coverage | TypeScript, Python, partial others | TypeScript, Python, Go, C# (Rust in beta) |
Prerequisites: Tools, SDKs, and Versions You Need
You don’t need a beefy machine to build an MCP server. Any laptop from the last five years running Node.js will do, since the server itself just handles JSON-RPC-style messages over HTTP. Here’s exactly what to install before starting Step 1.
- Node.js 20 LTS or newer (the MCP TypeScript SDK targets modern Node runtimes)
- npm 10+ or pnpm 9+ for package management
- The official MCP TypeScript SDK, which speaks the 2026-07-28 spec out of the box
- Python 3.11+ only if you also want to try the Python reference servers via
uvx - Docker Desktop or Docker Engine 26+ for the containerization step
- The MCP Inspector (installed via npx, no separate download needed)
- LangGraph SDK
langgraph-sdk 0.4.4(released August 27, 2026) if you’re following the LangGraph integration step - OpenAI Agents SDK
v0.22.0for Python orv0.17.0for TypeScript (both released August 19, 2026) - A code editor with TypeScript support (VS Code, Cursor, or similar)
- A free account on a container registry (Docker Hub or GitHub Container Registry) for the deployment step
All four Tier 1 MCP SDKs, TypeScript, Python, Go, and C#, already speak the 2026-07-28 spec, with a Rust SDK available in beta according to the 2026-07-28 specification announcement. This tutorial uses TypeScript because it has the deepest tooling support right now, but the same steps map onto the Python SDK with minor syntax differences.
Step 1: Scaffold Your MCP Server Project
Start with a clean directory and initialize a TypeScript project. This is standard Node.js setup, nothing MCP-specific yet.
mkdir weather-mcp-server && cd weather-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init --target es2022 --module node16 --outDir dist --rootDir src
Create a src folder with a single entry file, src/index.ts. Every MCP server example you’ll find in the official reference servers repository follows this same shape: import the SDK, instantiate a server, register tools, and start a transport. The 2026-07-28 spec doesn’t change this scaffolding, it changes what happens on the wire underneath it, so this step looks familiar even if you’ve built an MCP server before.
Add a package.json script so you can run the server without memorizing the tsx command every time.
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Step 2: Define Tool Schemas With Zod
MCP tools describe their inputs with a JSON schema so an agent can figure out how to call them without a human reading the docs first. The TypeScript SDK uses Zod to define that schema in code, then generates the JSON schema automatically. This is the part of an MCP server that actually matters for usability: a badly described tool gets ignored or misused by the calling agent, no matter how good the underlying implementation is.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "weather-mcp-server",
version: "1.0.0"
});
server.registerTool(
"get_forecast",
{
title: "Get Weather Forecast",
description: "Returns a short-range forecast for a given city and country code.",
inputSchema: {
city: z.string().describe("City name, e.g. Austin"),
countryCode: z.string().length(2).describe("ISO 3166-1 alpha-2 country code"),
days: z.number().int().min(1).max(7).default(3)
}
},
async ({ city, countryCode, days }) => {
const forecast = await fetchForecast(city, countryCode, days);
return {
content: [{ type: "text", text: JSON.stringify(forecast, null, 2) }]
};
}
);
Keep tool descriptions specific. “Get weather data” is worse than “Returns a short-range forecast for a given city and country code” because agent frameworks like LangGraph and the OpenAI Agents SDK pass that description straight to the underlying model when deciding whether to call the tool. Vague descriptions cause the calling model to either skip a genuinely useful tool or invoke the wrong one.
Step 3: Implement the Stateless Request Handler
This is where the 2026-07-28 spec actually shows up in your code. Instead of a transport that expects a long-lived connection and a session object, you wire the server to a stateless HTTP transport that treats every incoming request as self-contained.
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined // stateless mode, no session tracking
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => {
console.log("MCP server listening on port 3000 (stateless mode)");
});
Setting sessionIdGenerator to undefined tells the SDK not to bother issuing or expecting a session ID, which is the direct code-level consequence of the handshake removal described earlier. Each POST to /mcp is handled independently, which is exactly what lets you scale this server horizontally later without touching a shared store.
Step 4: Add a Resource Endpoint for Structured Data
Tools are for actions, resources are for data an agent can read without triggering a side effect. A well-rounded MCP server usually exposes both. Add a resource that surfaces the last N forecasts your server generated, useful for debugging and for agents that want to reference recent context without re-calling the tool.
server.registerResource(
"recent-forecasts",
"forecasts://recent",
{
title: "Recent Forecasts",
description: "The last 20 forecasts served by this MCP server",
mimeType: "application/json"
},
async () => ({
contents: [{
uri: "forecasts://recent",
mimeType: "application/json",
text: JSON.stringify(getRecentForecasts(), null, 2)
}]
})
);
Resources and tools share the same stateless request model under the 2026-07-28 spec, so nothing about scaling changes between them. The distinction is purely about intent: tools mutate or fetch fresh data, resources expose something the server already has on hand.
Step 5: Test Locally With the MCP Inspector
Before wiring anything into an agent framework, confirm the server actually works using the official MCP Inspector, a browser-based tool for calling your server directly and inspecting the raw request and response payloads.
npm run dev
# in a second terminal
npx @modelcontextprotocol/inspector http://localhost:3000/mcp
The Inspector opens a local web UI where you can list available tools, fill in arguments through a generated form, and fire off a call. You should see get_forecast listed with the schema you defined in Step 2. If the tool doesn’t show up, the most common cause is the server crashing silently on the server.connect(transport) call, usually because of a typo in the Zod schema. Check your terminal output before assuming the Inspector itself is broken.
Step 6: Write an Automated Smoke Test
Manual testing through the Inspector is fine for development but you want an automated check before every deploy. A minimal smoke test just POSTs a valid MCP request and checks for a 200 response with the expected tool result.
import { describe, it, expect } from "vitest";
describe("weather-mcp-server", () => {
it("responds to a tools/call request for get_forecast", async () => {
const res = await fetch("http://localhost:3000/mcp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "get_forecast",
arguments: { city: "Austin", countryCode: "US", days: 3 }
}
})
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.result.content[0].type).toBe("text");
});
});
Run this in CI on every pull request. Because the server is stateless, the test doesn’t need to worry about connection reuse or teardown ordering, which is one of the quieter benefits of the 2026-07-28 spec: your test suite gets simpler along with your deployment story.
Example Output: What a Working Tool Call Returns
It helps to know exactly what a healthy response looks like before you start debugging one that isn’t. When the smoke test from Step 6 passes, the raw JSON-RPC response coming back from your MCP server over the /mcp endpoint looks like this:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"city\": \"Austin\",\n \"countryCode\": \"US\",\n \"days\": 3,\n \"forecast\": [\n { \"day\": 1, \"high\": 91, \"low\": 74, \"condition\": \"Sunny\" },\n { \"day\": 2, \"high\": 89, \"low\": 73, \"condition\": \"Partly Cloudy\" },\n { \"day\": 3, \"high\": 86, \"low\": 71, \"condition\": \"Thunderstorms\" }\n ]\n}"
}
],
"isError": false
}
}
Notice there’s no session token, no connection ID, nothing tying this response back to a specific server process. That’s the stateless model working as intended: any replica behind your load balancer could have generated this exact payload, and the calling agent has no way to tell the difference, nor does it need to.
If you’re testing through the MCP Inspector instead of curl or a smoke test, the UI renders this same payload as a formatted tree rather than raw JSON, with the content array expanded so you can click into the text field directly. Either way, the shape of a correct response is identical: a content array containing one or more typed blocks, and an isError flag set to false. When something goes wrong inside your tool handler, set isError to true and put a human-readable message in the text block rather than letting the request fail with a raw 500, since agent frameworks handle a well-formed error response far more gracefully than a connection failure.
Step 7: Connect the Server to LangGraph 0.4.4
LangGraph’s Python SDK reached version 0.4.4 on August 27, 2026, and it treats MCP servers as first-class tool sources through its MCP adapter. Install the adapter and point it at your running server.
pip install langgraph==0.4.4 langchain-mcp-adapters
# agent.py
from langgraph.prebuilt import create_react_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"weather": {
"url": "http://localhost:3000/mcp",
"transport": "streamable_http"
}
})
tools = await client.get_tools()
agent = create_react_agent("openai:gpt-5.6-sol", tools)
result = await agent.ainvoke({
"messages": [{"role": "user", "content": "What's the 3-day forecast for Austin, US?"}]
})
print(result["messages"][-1].content)
Because your server runs in stateless mode, MultiServerMCPClient can open a fresh connection per call without any session negotiation overhead. If you were still running a pre-2026-07-28 server, this same client code would work, but you’d be paying for a handshake on every single tool call instead of just sending the request directly. If you’re deciding between LangGraph and other orchestration options for a larger project, our LangChain vs LangGraph comparison breaks down where each one fits.
Step 8: Connect the Server to the OpenAI Agents SDK
The OpenAI Agents SDK hit v0.22.0 for Python and v0.17.0 for TypeScript, both released August 19, 2026. Both are provider-agnostic, meaning they work with the OpenAI Responses and Chat Completions APIs as well as more than 100 other LLMs, so wiring in an MCP server isn’t locked to a single model vendor.
pip install openai-agents==0.22.0
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp
async def main():
async with MCPServerStreamableHttp(
params={"url": "http://localhost:3000/mcp"}
) as mcp_server:
agent = Agent(
name="Forecast Assistant",
instructions="Use the weather tool to answer forecast questions.",
mcp_servers=[mcp_server]
)
result = await Runner.run(agent, "3-day forecast for Austin, US")
print(result.final_output)
The MCPServerStreamableHttp class exists specifically to speak the transport this tutorial built in Step 3. If you’re following the TypeScript version of the Agents SDK instead, the equivalent class lives under the same mcp namespace with near-identical constructor arguments. For the model itself, see our walkthrough on using the GPT-6 Astra API if you want to swap in the newest OpenAI model instead of an older one.
Step 9: Add Bearer Token Authentication
A stateless server that anyone on the internet can call is a liability, not a convenience. Add a simple bearer token check as middleware before the request ever reaches the MCP transport. This stays compatible with the stateless model because the token is validated fresh on every request, there’s no session to attach the auth result to anyway.
const REQUIRED_TOKEN = process.env.MCP_AUTH_TOKEN;
app.use("/mcp", (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== `Bearer ${REQUIRED_TOKEN}`) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
});
For anything beyond a personal project, swap the static token for OAuth 2.1 client credentials, which is the pattern WorkOS’s analysis of the 2026 spec recommends for production MCP deployments. The stateless request model actually makes this easier than it used to be, since every request already carries its own identity metadata in _meta, giving you a natural place to attach and verify a token per call rather than per session.
Step 10: Containerize the Server With Docker
Package the server into a container so it can run identically on your laptop and in production. Because there’s no local state to worry about, the Dockerfile is unremarkable, which is exactly the point.
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist/ ./dist/
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/index.js"]
npm run build
docker build -t weather-mcp-server:1.0.0 .
docker run -p 3000:3000 -e MCP_AUTH_TOKEN=your-secret-here weather-mcp-server:1.0.0
Push the image to your registry of choice once it runs cleanly locally. Nothing here is MCP-specific yet, but it’s the foundation the next step builds on.
Step 11: Deploy Behind a Stateless Load Balancer
This is the step that used to require real engineering effort before the 2026-07-28 spec, and now doesn’t. Spin up multiple replicas of the same container and drop a plain round-robin load balancer in front of them. No sticky sessions, no Redis-backed session store, no consistent hashing scheme to keep a client pinned to the process that remembers it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: weather-mcp-server
spec:
replicas: 3
selector:
matchLabels:
app: weather-mcp-server
template:
metadata:
labels:
app: weather-mcp-server
spec:
containers:
- name: weather-mcp-server
image: your-registry/weather-mcp-server:1.0.0
ports:
- containerPort: 3000
env:
- name: MCP_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: mcp-secrets
key: auth-token
---
apiVersion: v1
kind: Service
metadata:
name: weather-mcp-server
spec:
selector:
app: weather-mcp-server
ports:
- port: 80
targetPort: 3000
type: LoadBalancer
Any of the three replicas can answer any incoming request, because none of them are holding onto conversation state. That’s the entire architectural payoff of the spec change condensed into one YAML file. Before the update, this same manifest would have needed session affinity annotations and a shared cache layer just to avoid breaking mid-conversation tool calls.
Step 12: Publish to the MCP Server Registry
Once your server is stable, list it so other developers and agent builders can discover it. The modelcontextprotocol/servers repository is the canonical reference point, updated on August 31, 2026 with release 2026.8.31. Community-maintained discovery sites also track server popularity by GitHub stars, giving you a rough sense of what’s actually getting adopted versus what’s just sitting in a repo unused.
If you want your server to work with zero setup for people who just want to try it, publish it as an npm package and document the npx invocation, following the same pattern the official Memory reference server uses.
npx -y @yourscope/weather-mcp-server
Python-based servers in the same official repository follow the equivalent pattern using uvx, for example uvx mcp-server-git for the Git reference server. Pick whichever runtime matches your target audience; TypeScript via npx tends to have a lower first-run friction for JavaScript-heavy teams, while uvx works well for Python shops that already have uv installed. If you’re building the server itself inside an AI-assisted editor, our guide to building Claude Code skills covers a related workflow for packaging reusable agent capabilities.
Framework and SDK Versions Referenced in This Tutorial
The AI agent ecosystem moves fast enough that version pinning matters. Here’s the exact state of the relevant tools as of early September 2026, so you know whether what you’re running matches what this tutorial describes.
| Tool / SDK | Latest Version | Release Date | Role in This Tutorial |
|---|---|---|---|
| MCP Specification | 2026-07-28 | July 28, 2026 | Core protocol, stateless request model |
| modelcontextprotocol/servers | 2026.8.31 | August 31, 2026 | Reference server implementations |
| LangGraph Python SDK | langgraph-sdk 0.4.4 | August 27, 2026 | Agent orchestration, Step 7 |
| OpenAI Agents SDK (Python) | v0.22.0 | August 19, 2026 | Agent orchestration, Step 8 |
| OpenAI Agents SDK (TypeScript) | v0.17.0 | August 19, 2026 | Alternative to Step 8 in TS projects |
| CrewAI | v1.15.18 | August 27, 2026 | Alternative multi-agent framework, not covered step-by-step here |
How an MCP Server Fits Into the Broader Agent Stack
It’s worth stepping back from the code for a moment to place this pattern in context. An MCP server isn’t a replacement for LangGraph, CrewAI, or the OpenAI Agents SDK, it’s the layer underneath all three. Those frameworks handle planning, memory, and multi-step reasoning; the MCP server is just the standardized way they reach out and touch the real world, whether that’s a weather API, a database, or an internal company tool. That separation of concerns is exactly why the 2026-07-28 spec update matters beyond the engineering details: it means the same weather server you build in this tutorial keeps working unchanged as agent frameworks themselves evolve, since it’s decoupled from any single framework’s internals.
If you’re running several different providers behind one agent, a multi-model AI router setup pairs naturally with the pattern here, since your MCP server stays identical regardless of which model is calling it. CrewAI’s v1.15.18 release, shipped August 27, 2026, is a good example of that decoupling in practice. The changelog for that release separates runtime context from the coding agent, a change that has nothing to do with MCP directly but benefits from it: because CrewAI can consume the same stateless MCP servers as LangGraph and the OpenAI Agents SDK, teams don’t have to write and maintain three separate integration layers just because they’re experimenting with three different orchestration frameworks. Build one MCP server correctly, and it becomes reusable infrastructure rather than a single-framework dependency.
This also changes how you should think about testing. A server that only gets exercised through one framework’s happy path can hide bugs that show up the moment a second framework calls it differently, for instance sending tool arguments in a slightly different order or omitting an optional field your handler assumed would always be present. Running the same server against both LangGraph and the OpenAI Agents SDK, as this tutorial does in Steps 7 and 8, is a cheap way to catch those assumptions before a third team building on your server finds them for you in production.
Common Pitfalls When Building MCP Servers
Most MCP server bugs fall into a small set of recurring categories. Here are the five that show up most often when developers build their first server against the 2026-07-28 spec.
- Mixing stateful and stateless code from old tutorials. Plenty of guides published before July 2026 still show the
initialize/initializedhandshake and session ID handling. Copying that pattern into a new server built on the current SDK version causes confusing runtime errors because the SDK no longer expects those messages. - Vague or missing tool descriptions. An agent framework decides whether to call your tool based entirely on the description string you wrote in Step 2. A one-word description like “weather” gets ignored far more often than a specific one describing exact inputs and outputs.
- Forgetting to validate tool arguments before using them. Zod schemas validate shape, not business logic. A
daysparameter capped at 7 in the schema still needs a runtime check if your upstream weather API only supports 5-day forecasts, otherwise you’ll get a confusing downstream error instead of a clean rejection. - Running the server in stateful mode by accident. If you leave
sessionIdGeneratorset to its default instead of explicitly setting it toundefined, you’ll get session behavior you didn’t ask for, and it will break the moment you deploy multiple replicas behind a load balancer. - Skipping authentication because “it’s just a demo.” Demo servers get left running on a public port more often than anyone wants to admit. Add the Step 9 bearer token check even for throwaway projects, since MCP servers exposed without auth checks have already been flagged by security researchers as a growing attack surface for agent-based systems.
Troubleshooting: 8 Issues You’ll Hit and How to Fix Them
Here’s a reference table for the errors that come up most often while building and deploying an MCP server, along with the fix for each.
| Symptom | Likely Cause | Fix |
|---|---|---|
| MCP Inspector shows no tools listed | Server crashed on startup before registering tools | Check terminal output for a stack trace, usually a Zod schema typo |
| 401 Unauthorized on every request | Bearer token mismatch between client and MCP_AUTH_TOKEN env var | Confirm the exact token value with no trailing whitespace |
| LangGraph agent never calls your tool | Tool description too vague for the model to match user intent | Rewrite the description with specific inputs and use cases |
| Works locally, fails after Docker deploy | Missing environment variables in the container runtime | Pass all required env vars via -e flags or a secrets manifest |
| Intermittent 500 errors under load | Server still holding state somewhere despite stateless transport config | Audit for in-memory caches keyed by session, replace with request-scoped data |
| OpenAI Agents SDK can’t connect to server | URL path mismatch, e.g. missing the /mcp suffix | Verify the exact endpoint path matches your Express route |
| Tool call succeeds but returns malformed content | Response object missing the required content array structure | Wrap all tool outputs in { content: [{ type: “text”, text: … }] } |
| Load balancer sends traffic to a replica that’s still starting | No readiness probe configured | Add a health check endpoint and a Kubernetes readinessProbe |
Advanced Tips for Production-Grade MCP Servers
Once the basic server works and passes your smoke tests, a few refinements separate a demo from something you’d trust in production.
Add structured logging keyed by request ID rather than session ID, since sessions no longer exist at the protocol level. Each incoming request should get a unique trace ID the moment it hits your middleware, and that ID should flow through every downstream call so you can reconstruct what happened when something breaks in production.
Rate limit per authenticated client rather than per IP address. Agent frameworks often run behind shared corporate NAT gateways or cloud provider egress IPs, so IP-based limits either block legitimate traffic or fail to catch the client actually causing load. Since every request already carries client identity in _meta under the new spec, use that field as your rate-limiting key instead.
Version your tool schemas explicitly. When you need to change a tool’s input shape, add a new tool name like get_forecast_v2 rather than mutating the existing one, since agents that cached the old schema description may still be calling it with the old argument shape for a while after your update ships.
Watch the twelve-month deprecation clock on Roots, Sampling, and Logging. If any part of your server or its dependencies still leans on those features, budget time to migrate before the support window closes, rather than discovering the hard way when a client library drops support ahead of the official cutoff. For a broader look at how the current model landscape compares before you pick which one to point your server at, see our AI model comparisons hub.
Finally, since the OpenAI Agents SDK and LangGraph are both provider-agnostic, test your server against more than one underlying model during development. A tool description that works well with one model’s function-calling behavior can get misinterpreted by another, and catching that early is cheaper than debugging it after a framework migration.
Set up distributed tracing across the whole call chain, not just inside your MCP server. When an agent framework calls your server, which then calls an upstream API, a single slow response can look like three different problems depending on which layer you’re staring at in isolation. Propagate a trace ID from the agent framework through your request middleware and into any downstream HTTP calls your tool handlers make, so a single dashboard can show the full path a request took instead of three disconnected logs you have to correlate by timestamp.
It’s also worth building a lightweight health check route separate from the /mcp endpoint itself. Kubernetes readiness and liveness probes shouldn’t have to speak the MCP protocol to know whether a replica is healthy, and hitting the real tool endpoint for a health check risks triggering side effects or burning quota against a rate-limited upstream API every few seconds. A plain /healthz route that returns 200 once the server has finished booting is enough for the orchestrator, and it keeps your actual MCP traffic metrics clean of synthetic probe noise.
The Complete Working Project
Putting all twelve steps together, your finished project structure looks like this:
weather-mcp-server/
├── src/
│ ├── index.ts # Server setup, transport, and route (Steps 1, 3)
│ ├── tools.ts # Tool schema and handler (Step 2)
│ ├── resources.ts # Resource endpoint (Step 4)
│ └── auth.ts # Bearer token middleware (Step 9)
├── tests/
│ └── smoke.test.ts # Automated smoke test (Step 6)
├── Dockerfile # Container definition (Step 10)
├── k8s-deployment.yaml # Load-balanced deployment (Step 11)
├── package.json
└── tsconfig.json
This exact structure is what powers the LangGraph integration from Step 7 and the OpenAI Agents SDK integration from Step 8, without any code duplication between the two. That’s the practical benefit of building to the 2026-07-28 spec from day one: the same stateless server works identically no matter which agent framework calls it, because neither framework needs to negotiate a session with your server before making a tool call.
From here, the natural next step is adding more tools to the same server rather than spinning up a new one for every capability. A single well-organized MCP server with five related tools is easier for an agent to reason about and easier for you to operate than five single-tool servers scattered across different deployments.
Frequently Asked Questions
Do I need to rebuild an existing MCP server for the 2026-07-28 spec?
Not immediately. Legacy HTTP+SSE transport and the deprecated features have a support window of roughly a year, so an existing stateful server keeps working. But new deployments should target the stateless model from the start, since it’s what current SDK versions and agent frameworks are optimized for, and migrating later means rewriting the same code you’d write correctly today.
Can I build an MCP server without TypeScript?
Yes. Python, Go, and C# are all Tier 1 SDKs that fully support the 2026-07-28 spec, and a Rust SDK is available in beta. The concepts in this tutorial, stateless transport, tool schemas, resource endpoints, map directly onto any of them, though exact syntax differs.
Why does my MCP server work in the Inspector but not with LangGraph?
This usually comes down to the transport type mismatch. Make sure your LangGraph client config specifies "transport": "streamable_http" and the exact URL your Express route listens on, including the /mcp path. The Inspector is more forgiving about small path mismatches than the LangGraph MCP adapter.
Is a stateless MCP server less capable than a stateful one?
No. Statelessness applies to the protocol layer, not your application logic. You can still maintain data in a database or cache that your tool handlers read from and write to, you just can’t rely on the MCP connection itself to remember anything between calls. Any state you need has to live somewhere your server explicitly manages, which is a more robust pattern than relying on connection-level session memory anyway.
How many tools should one MCP server expose?
There’s no hard limit in the spec, but grouping related tools into one server (like a weather server with forecast, historical data, and alerts tools) keeps deployment simpler than running a separate server per tool. The tradeoff is blast radius: if one tool handler has a bug, it can affect the whole server’s availability, so weigh that against operational overhead for your specific use case.
Does the OpenAI Agents SDK require an OpenAI model to use my MCP server?
No. Both the Python and TypeScript versions of the OpenAI Agents SDK are provider-agnostic, supporting the OpenAI Responses and Chat Completions APIs alongside more than 100 other LLMs. Your MCP server doesn’t need to know or care which model is calling it.
What happens to servers still using the old Mcp-Session-Id header?
They keep working for now, since the legacy transport has a one-year off-ramp from the July 28, 2026 release date. New clients built against current SDK versions may not send that header at all, though, so a hybrid server should be able to handle requests both with and without it during the transition period.


