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.