Simple MCP Server Example in Node.js
Build a local MCP server in Node.js with one working tool. Set up the current TypeScript SDK, run it over stdio, test it, and troubleshoot common issues.
To create a simple MCP server in Node.js, use the current TypeScript SDK package @modelcontextprotocol/server, register a tool with an input schema and handler, then serve it over stdio. The example below exposes a greet tool that accepts a name and returns a text response.
This tutorial follows the current TypeScript SDK v2 docs. Older examples may use the v1 monolithic @modelcontextprotocol/sdk package; for new code, the current docs use split packages such as @modelcontextprotocol/server. The SDK documentation identifies v2 as its stable line implementing the 2026-07-28 MCP specification. See the official TypeScript SDK repository and its first-server guide.
1. Create the Node.js project
The official first-server guide requires Node.js 20 or later. It uses ES modules because the SDK ships as ES modules, and tsx runs TypeScript directly without a separate compile step.
mkdir simple-mcp-server
cd simple-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
Save the following as src/index.ts. This is a complete stdio server using the v2 API shape:
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
serveStdio(() => {
const server = new McpServer({
name: 'hello-server',
version: '1.0.0',
});
server.registerTool(
'greet',
{
description: 'Greet someone by name',
inputSchema: { name: z.string() },
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }],
}),
);
return server;
});
// stdio stdout is reserved for MCP protocol messages.
console.error('hello MCP server running on stdio');
Run it from the project root:
npx tsx src/index.ts
The process waits for an MCP client to send requests over stdin. Its readiness message goes to stderr, where it does not interfere with protocol messages.
2. Understand the tool registration
registerTool(name, config, handler) connects three parts:
- Name:
greet, the identifier a client uses when listing and calling tools. - Configuration: a human-readable description and
inputSchema, here a single required string namedname. - Handler: an async function that receives validated input and returns content blocks. This handler returns one text block.
The Zod schema describes the input and lets the SDK validate tool arguments. For a single field, an object-shaped schema is concise. For cross-field rules, defaults, or more involved validation, use a Zod object and its normal validators, for example z.object({ name: z.string().min(1) }). Keep tool inputs narrow and explicit so the client can present the tool accurately and invalid arguments fail before application logic runs.
3. Test the server with MCP Inspector
You can test a stdio server without first registering it in an MCP host. Run the Inspector with the server launch command:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
In the Inspector, connect to the launched server, open its tools view, select greet, and provide a name. The result should contain Hello, <name>!. The official first-server walkthrough also demonstrates this Inspector workflow.
4. Choose the right transport
| Transport | Use it when | What it means operationally |
|---|---|---|
| stdio | A local host launches your server as a child process. | The host owns the process lifecycle; requests arrive on stdin and responses are written to stdout. |
| Streamable HTTP | You need a remotely reachable server endpoint for clients. | You host an HTTP endpoint and make deployment, networking, and session choices for that service. |
This example uses stdio because it is the simplest fit for a local integration. For remote hosting, use the SDK’s Streamable HTTP route and its serving guide rather than exposing a local stdio process directly. The stdio guide and HTTP guide describe the respective serving models. The docs do not provide comparative performance benchmarks, so choose based on where the server runs and how clients reach it.
5. Keep stdout clean and manage shutdown
For stdio, stdout is the protocol channel. Do not use console.log for diagnostics: even one plain-text line can make a host’s JSON-RPC parser reject the stream. Use console.error for logs, or send diagnostics through an appropriate MCP logging mechanism when your implementation needs structured logs.
When stdin closes, the transport can tear down with the client connection. If your server creates a timer, file watcher, connection pool, or another handle that keeps Node’s event loop alive, release it on connection close. For example:
serveStdio(() => {
const server = new McpServer({ name: 'hello-server', version: '1.0.0' });
const heartbeat = setInterval(() => {
console.error('server is still running');
}, 60_000);
server.server.onclose = () => clearInterval(heartbeat);
return server;
});
Only add long-lived handles when the server needs them. An unnecessary timer or watcher can make a process appear stuck after its client exits.
6. Configuration and common extensions
Server identity
The McpServer constructor takes a server name and version. Use a stable name that describes the integration and update the version when you release a new server build. The host sees this identity during connection setup.
Tool schema and results
Give every tool a distinct, action-oriented name and a description that makes its purpose and limits clear. Define the accepted inputs in the schema. Return content blocks in the handler; this example returns a text block. For anticipated failures, return a result the model can understand and mark tool errors using the SDK’s supported result shape rather than leaking stack traces or secrets. The SDK guide explains registration and result behavior in its server guide.
Adding tools
Register additional tools on the same server before returning it from the factory. Keep each handler focused. If a handler calls an external service, validate user-supplied inputs, set a request timeout, handle network and service errors, and return a concise useful message.
Local process configuration
An MCP host needs the executable and arguments used to start the process, plus any required environment variables. For this project, the launch command is npx with arguments tsx and src/index.ts. Configure that command in the host’s own MCP settings format; the precise file and schema vary by host. Keep credentials in environment variables or a secret store rather than hard-coding them in the source file.
When to use HTTP
For a shared remote endpoint, the current SDK docs use Streamable HTTP. HTTP introduces hosting and session decisions that a local stdio example does not need. Follow the official HTTP serving guide for handler setup and the adapter that matches your runtime. Older tutorials may show HTTP+SSE for earlier SDK generations; check the version of the SDK and protocol those examples target before copying their transport code.
7. cURL, Python, and Node.js client examples
The server above speaks MCP over stdio, so a raw HTTP request such as cURL or Python requests cannot call it directly. A client must speak MCP and connect through a compatible transport. These examples show the general client shape with the SDK generation specified in each example; use the matching client package and protocol documentation for your project.
Node.js MCP client over stdio (current v2 package layout)
For a separate Node.js client process, install the client package:
npm install @modelcontextprotocol/client
Then launch the TypeScript server process and connect over stdio using the client transport API documented for your installed SDK version. See the official SDK client documentation and examples; client transport APIs can change between SDK generations, so keep client and server examples on the same generation. The Inspector command above is the shortest runnable way to call this exact server.
cURL and Python
cURL and Python’s requests are HTTP tools; they do not implement the stdio MCP client handshake. They become relevant if you deploy an HTTP server and use a client that implements the selected MCP HTTP transport and protocol exchanges. For plain HTTP deployment, follow the official Streamable HTTP guide rather than sending an arbitrary GET or POST and assuming it is an MCP call. No generic cURL invocation can call this local stdio example as written.
Or skip the browser setup
If your MCP tool needs a website screenshot, ScreenshotNeo provides a screenshot API and an MCP server for AI agents, including Claude, Cursor, and other MCP clients. A single GET request returns a PNG, JPEG, WebP, or PDF. The API accepts a URL and returns the capture; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.
8. Performance, reliability, and cost
- Server overhead: this greeting server does no network I/O and has minimal work per call. Actual latency depends on the handler’s work, client, and transport; the SDK docs provide no benchmark for this sample.
- External calls: put timeouts on downstream requests, handle non-success responses, and avoid retrying non-idempotent operations blindly. Return errors in a form the model can act on.
- Process reliability: a stdio server’s host starts and stops the process. Keep startup deterministic, write diagnostics to stderr, and release resources on close.
- Cost: the example uses open-source Node packages and has no metered service dependency. If tools call paid APIs, their usage charges are separate from MCP itself.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Import or module syntax error | The project is not configured as an ES module, or the installed package/API generation differs from the example. | Set "type": "module", use Node.js 20 or later, and verify that you installed @modelcontextprotocol/server for the v2 example. |
| Host reports malformed JSON-RPC or fails during initialization | Text was written to stdout, often by console.log. |
Move diagnostic output to console.error and ensure dependencies or startup scripts do not print to stdout. |
| Tool does not appear in the client | The host may be launching the wrong path or command, or the server may fail before registering the tool. | Run npx tsx src/index.ts from the project root, then try the Inspector command. Check stderr for startup errors. |
| Tool call is rejected for invalid arguments | The client sent values that do not match the input schema. | Inspect the tool schema and provide the required string field name. Add a clearer description if callers misunderstand the expected input. |
| Process does not exit after the host disconnects | A timer, watcher, socket, or pool still holds the Node event loop open. | Close the resource on server shutdown or connection close; avoid creating keep-alive resources the tool does not need. |
| Cannot connect with cURL or Python requests | The example uses stdio, not HTTP. | Use an MCP client over stdio, such as the Inspector, or deploy a Streamable HTTP server and use a client that supports that transport. |
| Legacy tutorial imports cannot be resolved | The tutorial targets v1’s monolithic package but the project has v2 packages, or vice versa. | Use a consistent SDK generation and follow the official migration guide when updating a v1 project. |
10. FAQ
Does an MCP server need an AI model inside it?
No. The server exposes capabilities such as tools. An MCP host or client connects to it and decides how a model uses those capabilities.
Can one server expose tools and other MCP capabilities?
Yes. The SDK server supports registering tools, resources, and prompts. This minimal example includes only one tool to keep the first connection easy to understand.
Can I use plain JavaScript instead of TypeScript?
The v2 SDK is usable from JavaScript, but this quickstart uses TypeScript and tsx because the official first-server path demonstrates that workflow. Keep the ES module setting and adapt the source and launch command for JavaScript.
Where should I start when upgrading an existing v1 server?
Use the official v1-to-v2 migration guide. Its package names and transport wiring differ from the new-server example here.


