Skip to main content
This lesson shows how to build a minimal stdio MCP (Model Context Protocol) server in Node.js. The goal is a tiny tool catalog exposing a single tool, add-integers, which adds two integers. You’ll walk through project initialization, a complete server.js implementation, and quick ways to test the server from the command line and Postman. Prerequisites:
  • Node.js installed (LTS recommended)

Quick overview

  • Initialize a Node project and install the MCP SDK.
  • Implement an MCP Server that:
    • exposes a tools/list catalog describing input schemas,
    • implements tools/call to run the add-integers tool,
    • communicates over stdio using StdioServerTransport.
  • Test via piping JSON into the script or using Postman.

Initialize the project

Create a new directory and initialize an npm project:
npm init -y
Install the MCP SDK:
npm install @modelcontextprotocol/sdk
A typical package.json for this example enables ES modules and provides a bin entry so you can run the server directly:
{
  "name": "basic-mcp-server",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "bin": {
    "basic-mcp-server": "./server.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.15.0"
  }
}
Make sure "type": "module" is present so Node.js accepts ES module import syntax in server.js.

Files and commands at a glance

ItemPurposeExample / Command
Project initCreate package filesnpm init -y
Install SDKAdd MCP SDK dependencynpm install @modelcontextprotocol/sdk
Run serverExecute script directlynode server.js or chmod +x server.js && ./server.js
Main fileMCP server implementationserver.js

Implement server.js

Create server.js in the project root. The file starts with a shebang so you can run it directly from the shell. Import the Server and transport classes and the request schemas from the SDK:
#!/usr/bin/env node

import { Server, StdioServerTransport } from "@modelcontextprotocol/sdk";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
Next, create a Server instance with basic metadata and an initial empty capabilities object:
const server = new Server(
  {
    name: "basic-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

Implement the tools catalog (tools/list)

When a client requests the tool catalog (tools/list), return an array of tools with a clear inputSchema. This helps clients know the expected types and required fields. Here we expose a single tool, add-integers, which expects two integer properties: a and b.
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "add-integers",
        description: "Add two integers and return the result",
        inputSchema: {
          type: "object",
          properties: {
            a: {
              type: "integer",
              description: "First integer to add",
            },
            b: {
              type: "integer",
              description: "Second integer to add",
            },
          },
          required: ["a", "b"],
        },
      },
    ],
  };
});
This schema gives clients machine-readable validation rules and clear human-readable descriptions.

Implement the tool execution (tools/call)

Handle tools/call requests by extracting the tool name and arguments, validating types, performing the operation, and returning a result in MCP content format. This example requires both arguments to be integers:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params || {};
  const a = args?.a;
  const b = args?.b;

  if (name === "add-integers") {
    if (typeof a !== "number" || typeof b !== "number") {
      throw new Error("Both arguments must be numbers");
    }

    if (!Number.isInteger(a) || !Number.isInteger(b)) {
      throw new Error("Both arguments must be integers");
    }

    const result = a + b;

    // Return result as text so clients can parse/convert as needed
    return {
      content: [
        {
          type: "text",
          text: `${result}`,
        },
      ],
    };
  }

  throw new Error(`Unknown tool: ${name}`);
});
Tip: returning content as text is broadly interoperable; callers that need numbers can parse or convert the text result.

Start the server using stdio transport

Use StdioServerTransport so the server communicates over standard input/output. Wrap startup in an async main and log to stderr to avoid polluting MCP JSON responses on stdout:
async function main() {
  try {
    const transport = new StdioServerTransport();
    await server.listen(transport);
    // Log to stderr so MCP JSON on stdout is not polluted by startup logs
    console.error("Basic MCP Server running on stdio");
  } catch (err) {
    console.error("Server error:", err);
    process.exit(1);
  }
}

main();
Make server.js executable if you want to run it directly:
chmod +x server.js
./server.js

Test the server from the command line

You can simulate MCP JSON-RPC requests by piping JSON messages into the running script. Because the server writes status messages to stderr, the stdout will contain only the JSON response (useful for automated tests).
  1. Test tools/list:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node server.js
Expected stdout (example):
{"result":{"tools":[{"name":"add-integers","description":"Add two integers and return the result","inputSchema":{"type":"object","properties":{"a":{"type":"integer","description":"First integer to add"},"b":{"type":"integer","description":"Second integer to add"}},"required":["a","b"]}}],"jsonrpc":"2.0","id":1}
  1. Call the add-integers tool:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add-integers","arguments":{"a":5,"b":3}}}' | node server.js
Expected stdout (example):
{"result":{"content":[{"type":"text","text":"8"}],"jsonrpc":"2.0","id":1}

Test with Postman

Postman supports MCP-style workflows and can invoke stdio transports or HTTP transports depending on how the client and server are configured.
  • Create a new request in Postman.
  • Choose the stdio transport and point it to the node server.js command (or the path to the executable).
  • Send a tools/list request to inspect the catalog and input schema.
  • Send a tools/call request with:
    • name: "add-integers"
    • arguments: { "a": 3, "b": 3 }
  • Postman will display the JSON-RPC responses similar to the command-line examples above.
If you prefer HTTP transport for testing or integration, you can adapt the server to an HTTP transport or use a proxy that translates HTTP requests to stdio MCP messages.

Recap and next steps

You now have a minimal, functioning MCP server in Node.js that:
  • Exposes a tools catalog with structured input schemas.
  • Implements tool execution with explicit input validation and clear error handling.
  • Communicates over stdio in standard MCP JSON-RPC format.
Next ideas to extend this project:
  • Add more tools (file operations, DB queries, external API calls).
  • Return richer content types (JSON objects, structured results, or binary data via attachments).
  • Add logging, metrics, and unit tests around the request handlers.
Thank you for following this lesson.

Watch Video