Unpacked: Building an MCP Server on the 2026-07-28 spec
The new MCP stateless spec is here, and yes, there are breaking changes. Most of the coverage has been about migration costs for existing servers, but before you hold off on building, remember this: starting fresh is a completely different story.
Today on Unpacked: One tool, one walkthrough, under 5 minutes.
What It Is
Monday’s issue covered MCP going stateless. That spec landed yesterday, and most of the coverage since has been about what breaks. Starting fresh is a different story.
The TypeScript SDK v2 owns the whole protocol surface, so you write a Zod schema and a function. No handshake code, no session management, no server/discover handler even though the spec now requires servers to have one. Node.js 20 or later is the only prerequisite, and there’s no build step.
Get It Running
mkdir weather && cd weather
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
type=module will be required on your package.json, since the SDK ships ES modules only. To help us, tsx executes TypeScript directly, no compile step needed.
Your First Tool
The whole server is a factory that builds an McpServer and registers one tool: a US weather alert lookup against the National Weather Service. Create src/index.ts.
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const NWS_API = 'https://api.weather.gov';
interface AlertsResponse {
features: { properties: { event?: string; headline?: string } }[];
}
function createServer(): McpServer {
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get-alerts',
{
description: 'Get the active weather alerts for a US state',
inputSchema: z.object({
state: z.string().length(2).describe('Two-letter US state code, e.g. CA')
})
},
async ({ state }) => {
const code = state.toUpperCase();
const url = `${NWS_API}/alerts/active?area=${code}`;
const res = await fetch(url, { headers: { 'User-Agent': 'mcp-weather-tutorial/1.0' } });
if (!res.ok) {
return { content: [{ type: 'text', text: `NWS API error: HTTP ${res.status}` }], isError: true };
}
const { features } = (await res.json()) as AlertsResponse;
if (features.length === 0) {
return { content: [{ type: 'text', text: `No active alerts for ${code}.` }] };
}
const lines = features.map(f => f.properties.headline ?? f.properties.event ?? 'Unnamed alert');
return { content: [{ type: 'text', text: lines.join('\n') }] };
}
);
return server;
}
The schema does the work
The registerTool function requires three parameters: a name, a configuration (config), and an asynchronous handler.
By constructing a Zod schema, the SDK generates three essential components: the JSON Schema utilized by the model, the runtime validation, and the argument types for the handler. The description provided through the .describe() method is incorporated into the schema accessed by the model. Validation is conducted prior to the execution of the handler.
For instance, if one invokes get-alerts with the argument { "state": "California" }, the execution of the code will not occur, and the user will receive an error message indicating a validation problem:
Input validation error: Invalid arguments for tool get-alerts: state: Too big: expected string to have <=2 characters
The model interprets this failure and is capable of self-correction. In instances where your code identifies failures, such as in the context of an HTTP error, the attribute isError: true fulfills a comparable function.
Serving it
Two lines at the end of the file hand the factory to the stdio transport:
void serveStdio(createServer);
console.error('weather MCP server running on stdio');
npx tsx src/index.ts
The banner writes to stderr and then nothing happens. That’s correct. An stdio server blocks on stdin waiting for a client to start talking.
Calling it without a host
You don’t need Claude Code or Cursor to try this. MCP Inspector will launch your server and provide you with a browser user interface.
Run the following command in your terminal:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
Once the interface is open, click on “Connect”. Navigate to the Tools tab, select get-alerts, enter a state code such as TX, and then run the command. You will receive a response in the same text format that a model would produce.
What Worked, What Didn’t
console.logcorrupts the stream. stdout is the protocol channel. Log withconsole.erroror your server will fail in ways that look like a client bug. The docs warn about it, but it’s still the fastest way to lose an hour.- The spec change is invisible from the server side. No initialize, no session id, no
server/discoverhandler, even though servers must now implement one. The SDK writes all of it for you. Greenfield builders get the new protocol free and only existing v1 servers pay the migration cost. - From the client side it’s the opposite. Every request carries a
_metaenvelope, and it has to be complete. Sendtools/callwithprotocolVersionbut noclientCapabilitiesand you get-32602 Invalid _meta envelope, even thoughserver/discoveraccepted the same partial envelope moments earlier. This is the payload cost the maintainers flagged, and you meet it the first time you hand-roll a client. - stdio is the easy path, not the deployable one. It works because a host launches your server as a local child process. Serving many clients means the same
createServerfactory over HTTP, which is where the stateless spec will shine.
When to create a MCP
Consider building a server if you need a model to access resources that are exclusive to you, such as an internal API, a staging database, or your deployment pipeline. Additionally, a MCP server can help you add more structure to a workflow. However, if a shell script meets your needs or if the task is a one-time occurrence, it may be best to skip this approach.