DEV Community

Cover image for What Is MCP, Really? A Real Server, Tested Against the Actual Protocol
Zahid Hasan Tonmoy
Zahid Hasan Tonmoy

Posted on Originally published at zahidhasantonmoy.vercel.app

What Is MCP, Really? A Real Server, Tested Against the Actual Protocol

MCP got one sentence in the first post of this series, as one reason agents got practical, with no real explanation. This post is that explanation, plus a server I actually ran the protocol against.

This post continues my series on building agents. The previous one covered AI agent vs Zapier/Make.

The problem MCP solves

Every tool in this series so far was wired directly into one project. Fine once — until a second project wants the same tool, and you're copying the function, schema, and description, keeping both in sync by hand. MCP's fix: expose a tool once, as a server, and any MCP-compatible client can use it without new integration code for that pairing.

flowchart TB
    subgraph "Without MCP: custom glue per pair"
        App1[App 1] -->|custom code| ToolA[Tool A]
        App1 -->|custom code| ToolB[Tool B]
        App2[App 2] -->|custom code| ToolA
        App2 -->|custom code| ToolB
    end
    subgraph "With MCP: one server, any client"
        AppX[App 1] -->|MCP| ServerA[Tool A as an MCP server]
        AppY[App 2] -->|MCP| ServerA
        AppX -->|MCP| ServerB[Tool B as an MCP server]
        AppY -->|MCP| ServerB
    end

Why now

Anthropic introduced MCP in November 2024. OpenAI and Google both adopted it within months. In December 2025, Anthropic donated the protocol to the Agentic AI Foundation, a vendor-neutral Linux Foundation project co-founded with Block and OpenAI. A single company's protocol became shared industry infrastructure.

A real server

// mcp/weather-server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const weatherData: Record<string, { condition: string; temp_c: number }> = {
  Dhaka: { condition: 'humid and cloudy', temp_c: 31 },
  Chittagong: { condition: 'light rain', temp_c: 29 },
};

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

server.registerTool(
  'get_weather',
  {
    title: 'Get Weather',
    description: 'Get the current weather condition and temperature in Celsius for a named city.',
    inputSchema: { city: z.string() },
  },
  async ({ city }) => {
    const data = weatherData[city];
    const result = data ? { city, ...data } : { error: `no weather data for ${city}` };
    return { content: [{ type: 'text', text: JSON.stringify(result) }] };
  },
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main();
Enter fullscreen mode Exit fullscreen mode

Same name, same description, same one field as the get_weather tool from the tool-calling post — inputSchema just takes Zod validators instead of hand-written JSON Schema, and the SDK converts it.

Testing it against the real protocol

// mcp/test-client.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function main() {
  const transport = new StdioClientTransport({ command: 'npx', args: ['tsx', 'mcp/weather-server.ts'] });
  const client = new Client({ name: 'test-client', version: '1.0.0' });
  await client.connect(transport);

  const tools = await client.listTools();
  console.log(tools);

  const result = await client.callTool({ name: 'get_weather', arguments: { city: 'Dhaka' } });
  console.log(result);

  await client.close();
}

main();
Enter fullscreen mode Exit fullscreen mode

This spawns the server as a real subprocess and does the actual MCP handshake — not a mock.

The bug that took a rewrite

The first version ended with a bare top-level await server.connect(transport). Type-checked fine. The client connection closed immediately on run. The cause: this file's execution transforms it to a module format that doesn't support top-level await, crashing the process before the server could listen — nothing to do with MCP itself, but exactly where an MCP server's entrypoint sits. Wrapping it in an async main() fixed it completely.

Common mistakes

  • Top-level await at the entrypoint — wrap it in an async function.
  • Treating a vague MCP tool description as lower-stakes than an inline one — it's the same field, now shared across every project that connects.
  • Connecting to a third-party server without reviewing what it exposes — a tool description is server-controlled text the model reads as an instruction.
  • Building a server for a tool that already has one in a public registry.

Read the full version

I write each post in English and Bangla. The full version is on my blog: What Is MCP?

Have you built or connected to an MCP server yet?

Top comments (0)