node-mcp
by styles102
README.md
# node-mcp
A training project for building MCP (Model Context Protocol) servers with Node.js and TypeScript.
## Stack
- Node.js with ES modules (`"type": "module"`)
- TypeScript (`NodeNext` module mode)
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) — official MCP TypeScript SDK
- [Zod](https://zod.dev) — input/output schema validation
- [Express](https://expressjs.com) — HTTP server for Streamable HTTP transport
- [`openweather-api-node`](https://github.com/loloToster/openweather-api-node) — OpenWeatherMap API client
- [`dotenv`](https://github.com/motdotla/dotenv) — environment variable loading
## Project Structure
```
node-mcp/
├── index.ts # Server entry point — registers all tools, selects transport
├── toolHandler.ts # Generic error-handling wrapper for tool handlers
├── tools/
│ ├── opperations/
│ │ ├── add.ts # Add tool
│ │ └── divide.ts # Divide tool
│ └── weather/
│ └── weather.ts # Weather tool
├── types.d.ts # Module declarations for untyped packages
├── .env # API keys and config (not committed)
├── dist/ # Compiled output (generated by tsc)
├── tsconfig.json
└── package.json
```
## Getting Started
```bash
pnpm install
```
Create a `.env` file in the project root:
```
OPEN_WEATHER_API_KEY=your_key_here
NODE_ENV=development
TRANSPORT=stdio # or "http"
MCP_PORT=3000 # optional, HTTP mode only
```
Build:
```bash
pnpm build
```
## Running the Server
### stdio mode (for Claude Desktop / Claude Code)
```bash
$env:TRANSPORT="stdio"; node dist/index.js
```
The server communicates over stdin/stdout and waits for JSON-RPC messages from an MCP client. No visible output when idle.
### HTTP mode (persistent process)
```bash
$env:TRANSPORT="http"; node dist/index.js
```
Starts an Express server on port 3000 (or `MCP_PORT`):
```
Starting Express MCP server on port 3000
MCP endpoint: http://localhost:3000/mcp
Server is running on http://localhost:3000
```
## Testing with MCP Inspector
### stdio mode
```bash
pnpm exec mcp-inspector node dist/index.js
```
### HTTP mode
Start the server first, then open the inspector without arguments:
```bash
pnpm exec mcp-inspector
```
In the inspector UI, set transport type to **Streamable HTTP** and URL to `http://localhost:3000/mcp`.
## Connecting to Claude Code
### stdio (project-level)
```bash
claude mcp add --scope project node-mcp-server node C:/Training/node-mcp/dist/index.js
```
### HTTP
Add the server URL directly in Claude Code's MCP settings pointing at `http://localhost:3000/mcp`.
## Tools
### `add`
Adds two numbers together and returns the result.
**Inputs**
| Name | Type | Description |
|----------|--------|----------------|
| `numOne` | number | First operand |
| `numTwo` | number | Second operand |
**Output**
```json
{ "result": "32 + 32 = 64" }
```
---
### `divide`
Divides two numbers and returns the result. Both inputs must be positive — validated by Zod before the handler runs.
**Inputs**
| Name | Type | Description |
|----------|-------------------|--------------------------|
| `numOne` | number (positive) | Dividend |
| `numTwo` | number (positive) | Divisor (cannot be zero) |
**Output**
```json
{ "result": "10 / 2 = 5" }
```
---
### `weather`
Returns current weather data for a location using the OpenWeatherMap API. Results are in metric units.
Supply **one** of the following location strategies:
| Name | Type | Description |
|----------------|----------------------|------------------------------------------|
| `lat` | number (-90 to 90) | Latitude (use with `lng`) |
| `lng` | number (-180 to 180) | Longitude (use with `lat`) |
| `locationName` | string | City or place name (e.g. `"London"`) |
| `zipCode` | string | Zip/postal code (e.g. `"90210"`) |
Priority order when multiple are provided: coordinates → zip code → location name. At least one strategy must be supplied or the tool returns an error.
**Output**
Returns a JSON object with temperature, humidity, wind speed, weather description, and more.
**Requires** `OPEN_WEATHER_API_KEY` in `.env`.
## Key Concepts
### Transport selection
The server supports two transports, selected via the `TRANSPORT` environment variable:
| Value | Use case |
|---------|-------------------------------------------------------|
| `stdio` | Local servers spawned as child processes (Claude Code, Claude Desktop) |
| `http` | Persistent process accessible over a network |
Tools are registered once and shared between both transports — the capability is decoupled from the delivery mechanism.
### Tool registration pattern
Each tool lives in its own file and exports a registration function that accepts the server instance:
```ts
export const myTool = (server: McpServer) => {
server.registerTool('tool-name', { ... }, toolHandler(async (inputs) => {
// handler logic
}))
}
```
In `index.ts`:
```ts
myTool(server)
```
### Error handling — `toolHandler`
All tool handlers are wrapped with `toolHandler`, a generic wrapper that catches any thrown error and returns it in the correct MCP format:
```ts
export const toolHandler = <T>(fn: (inputs: T) => Promise<CallToolResult>) => {
return async (inputs: T) => {
try {
return await fn(inputs)
} catch (err) {
return {
isError: true,
content: [{
type: 'text' as const,
text: err instanceof Error
? process.env.NODE_ENV === 'development' ? err.stack ?? err.message : err.message
: 'Something went wrong...'
}]
}
}
}
}
```
- In development (`NODE_ENV=development`): returns the full stack trace
- In production: returns the error message only
- For non-Error throws: returns a generic fallback message
### Input validation with Zod
Zod schemas on `inputSchema` are validated by the SDK before your handler runs. Invalid inputs return a `-32602` protocol error — they never reach your handler:
```ts
inputSchema: {
numTwo: z.number().positive() // rejects zero and negatives
numTwo: z.number().refine(n => n !== 0) // rejects only zero
}
```
### Streamable HTTP transport
The HTTP transport uses Express with the SDK's `StreamableHTTPServerTransport`. Key points:
- `sessionIdGenerator: randomUUID` — required, assigns a unique ID to each client session
- `express.json()` middleware parses the request body before it reaches the transport
- The parsed body is passed as the third argument to `handleRequest` — the SDK uses it directly rather than re-parsing the raw request
```ts
app.use(express.json())
app.all('/mcp', (req, res) => {
transport.handleRequest(req, res, req.body)
})
```
### ES module import paths
With `"module": "NodeNext"` in tsconfig, imports require explicit `.js` extensions even in `.ts` source files:
```ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
```
### Environment variables
Loaded via `import 'dotenv/config'` at the top of `index.ts`. Add `.env` to `.gitignore` to avoid committing API keys.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues