Skip to main content

Chapter 16: Creating a Node.js MCP Server

TL;DR

Below is a complete, minimal index.js for an MCP server using Express.

const express = require('express');

// 1. Define your tool logic as functions.
const greet = (params) => {
if (!params.name) {
throw new Error('Missing \'name\' parameter');
}
return { greeting: `Hello, ${params.name}!` };
};

// A map of tool names to functions.
const toolHandlers = {
greet: greet,
};

// 2. Create an Express server.
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies

// 3. Create a single route to handle all tool calls.
app.post('/', (req, res) => {
const { method, params } = req.body;

if (method !== 'tool.call') {
return res.status(400).json({ error: 'Unsupported method' });
}

const handler = toolHandlers[params.name];

if (handler) {
try {
const result = handler(params.arguments);
// 4. Send a successful JSON-RPC response.
res.json({ jsonrpc: '2.0', result });
} catch (e) {
// 5. Send an error response.
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32602, message: e.message }
});
}
} else {
res.status(404).json({
jsonrpc: '2.0',
error: { code: -32601, message: 'Tool not found' }
});
}
});

// 6. Start the server.
const port = 8080;
app.listen(port, () => {
console.log(`Node.js MCP Server listening on port ${port}`);
});

Node.js is an excellent choice for building MCP servers, especially for developers who are already comfortable in the JavaScript/TypeScript ecosystem or whose tools are I/O-heavy.

We will use the popular Express web framework to build our server, but any framework that can handle HTTP requests will work.

Step 1: Setting up the Project

Your package.json file should include express as a dependency. The worka init command will set this up for you if you choose the Node.js template.

{
"name": "my-pack-svc",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.19.2"
}
}

Step 2: Creating the HTTP Server

In your index.js file, you'll start by creating a basic Express application. The most important piece of middleware you need is express.json(), which automatically parses the JSON body of incoming requests.

const express = require('express');
const app = express();
app.use(express.json());

const port = 8080;
app.listen(port, () => {
console.log(`Node.js MCP Server listening on port ${port}`);
});

Step 3: Handling the tool.call Request

Your server only needs a single route: a POST handler that will receive all MCP requests from the Worka Host. Inside this handler, you will parse the request to determine which tool to run.

app.post('/', (req, res) => {
// Extract the method and params from the JSON-RPC request
const { method, params } = req.body;

if (method !== 'tool.call') {
return res.status(400).json({ error: 'Unsupported method' });
}

const toolName = params.name;
const toolArgs = params.arguments;

// ... (we will add routing logic next)
});

Step 4: Implementing and Routing Tools

It's good practice to define your tool logic in separate functions and then use a map or a switch statement to call the correct one.

Let's implement our greet tool and a simple router.

// Define the logic for the greet tool
const greet = (params) => {
if (!params || !params.name) {
// It\'s good practice to validate your parameters
throw new Error('Missing \'name\' parameter');
}
return { greeting: `Hello, ${params.name}!` };
};

// Create a map to hold all your tool handlers
const toolHandlers = {
greet: greet,
// another_tool: anotherToolFunction,
};

// Inside your app.post('/', ...) handler:
const handler = toolHandlers[toolName];

if (handler) {
try {
const result = handler(toolArgs);
// Send a successful response
res.json({ jsonrpc: '2.0', result });
} catch (e) {
// Send an error response if the tool logic throws an error
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32602, message: e.message }
});
}
} else {
// Handle the case where the tool name doesn't exist
res.status(404).json({
jsonrpc: '2.0',
error: { code: -32601, message: 'Tool not found' }
});
}

This structure makes it easy to add new tools. Simply write a new function and add it to the toolHandlers object. The worka dev process will automatically restart your Node.js server when you save your changes, giving you the same hot-reloading experience as with Rust servers.