MCP server todo application - Filip Brebera (hiring task)
This is my solution to the task of implementing a multi-node todo application using an MCP server with Redis for storage and OpenRouter for AI-powered analysis.
Prerequisites
Required Tools:
OpenRouter API Key:
You need an OpenRouter API key to run the AI analysis feature.
- Get your API key from openrouter.ai/keys
- Copy the example environment file:
cp .env.example .env
- Edit
.envand add your API key:
OPENROUTER_API_KEY=sk-or-v1-your-actual-key-here
Quick Start
Local Development (Stdio Mode)
For testing with MCP clients like VS Code or Cursor: Note: Make sure MCP_MODE=stdio is set in your .env file.
# Install dependencies
bun install
# Start Redis
docker run -d -p 6379:6379 redis:7-alpine
# Run in stdio mode
bun run dev
Development (Multi-Node HTTP)
For testing multi-node setup locally with hot reload:
# Start all services with hot reload
docker-compose -f docker-compose.dev.yml up
# Test health
curl http://localhost:3000/health
Production (Multi-Node HTTP)
For production deployment:
# Build and start all services
docker-compose up --build -d
# Verify health
curl http://localhost:3000/health
# View logs
docker-compose logs -f
Server Modes
You can switch between two modes - stdio and HTTP - using the MCP_MODE environment variable.
Stdio Mode: For local development and MCP client integration (VS Code, Cursor)
- Single user session
- Direct stdio communication
- No HTTP server
HTTP Mode: For multi-node production deployment
- Distributed sessions via Redis
- Load balanced across multiple nodes
- HTTP transport with session headers
Available Tools
| Tool | Parameters | Description | |------|------------|-------------| | todo_add | title (string), priority? (1-5) | Add a new todo | | todo_list | filter? (all/pending/completed) | List todos | | todo_remove | id (UUID) | Remove a todo | | todo_mark_done | id (UUID) | Mark todo as completed | | todo_clear | - | Clear all todos | | todo_analyze | model? (AI model name) | AI-powered prioritization |
Configuration
| Variable | Default | Description | |----------|---------|-------------| | MCP_MODE | stdio | Server mode: stdio or http | | SERVER_PORT | 3000 | HTTP server port (http mode only) | | REDIS_URL | redis://localhost:6379 | Redis connection string | | OPENROUTER_API_KEY | required | API key from openrouter.ai | | NODE_ID | unknown | Node identifier (for debugging) |
Testing HTTP Mode
Sessions are created automatically on first request. MCP clients handle sessions transparently.
With MCP Client (Claude Code, VS Code, Cursor)
Use the included .mcp.json.http.example template:
# Copy the HTTP example
cp .mcp.json.http.example .mcp.json
# Start the multi-node setup
docker-compose up --build -d
The config file points to http://localhost:3000/mcp (load-balanced endpoint).
Testing Stdio Mode
Quick Setup (Recommended)
The project includes a .mcp.json.example template that MCP clients (Claude Code, VS Code, Cursor) automatically detect.
Steps:
- Copy the example config:
cp .mcp.json.example .mcp.json - Edit
.mcp.jsonand update the absolute path and OpenRouter API key - Start Redis:
docker run -d -p 6379:6379 redis:7-alpine - Restart your MCP client - tools will be available automatically
Manual Setup
Alternatively, add to your MCP client settings manually: Note: Make sure MCP_MODE=stdio is set in your .env file.
{
"mcpServers": {
"todo": {
"command": "bun",
"args": ["run", "/absolute/path/to/src/main.ts"],
"env": {
"OPENROUTER_API_KEY": "your-key-here"
}
}
}
}
Testing with MCP Inspector
The MCP Inspector provides a visual UI for testing MCP servers:
HTTP Mode
# Start the dev environment
docker-compose -f docker-compose.dev.yml up
# In another terminal, start inspector
npx @modelcontextprotocol/inspector http://localhost:3000/mcp
# Open the URL shown (usually http://localhost:6274)
Stdio Mode
# Start Redis
docker run -d -p 6379:6379 redis:7-alpine
# Start inspector with stdio
npx @modelcontextprotocol/inspector bun run src/main.ts
Note: Make sure MCP_MODE=stdio is set in your .env file for stdio mode to work correctly.
The inspector will open in your browser where you can:
- View all available tools
- Test tool calls with validation
- See input/output schemas
- Debug responses
Approach and Key Decisions
I have decided to implement the server in TypeScript using Bun and Hono as a web server for its performance and developer experience. I used the official MCP SDK for TypeScript to handle MCP protocol details. As Redis client, I chose ioredis for its robustness and features. For AI integration, I used Vercel's AI SDK with OpenRouter as a provider to keep it flexible and try out different models.
Multi-Node Session Management
The main challenge was implementing multi-node support without sticky sessions while working within the MCP SDK's limitations.
The Problem:
- The MCP SDK's built-in session management is designed for single-node deployments
- The SDK stores sessions in memory, which doesn't work across multiple nodes
- Without sticky sessions, requests can land on any node
The Solution:
- AsyncLocalStorage for dependency injection of
sessionIdto tool handlers - Stateless SDK transport (
WebStandardStreamableHTTPServerTransport) with nosessionIdGenerator - Auto-session creation on first request with
Mcp-Session-Idheader tracking
Why AsyncLocalStorage?
The SDK's tool handlers don't support custom parameters, so we can't pass sessionId directly: ```typescript // SDK API doesn't allow this: server.registerTool('todo_add', config, async (args, sessionId) => { ... })
// We have to use: server.registerTool('todo_add', config, async (args) => { const sessionId = sessionContext.getStore(); // AsyncLocalStorage }) ```
AsyncLocalStorage provides ambient context that propagates through the async call chain without modifying function signatures.
Alternative Approaches Considered:
- Hardcode SDK responses - Build JSON-RPC responses manually instead of using the SDK
- ❌ More code to maintain
- ❌ Lose SDK benefits (validation, protocol handling)
- ✅ Full control over session handling
- Modify SDK source - Fork the SDK to add session parameter support
- ❌ Maintenance burden
- ❌ Out of sync with upstream
- ✅ Cleaner API
- Sticky sessions - Use load balancer sticky sessions
- ❌ Violates assignment requirements
- ❌ Does not scale that well
- ✅ Simpler implementation
AI Model response JSON Parsing
Initially attempted to use Vercel AI SDK's structured output (Output.array()), but discovered that most models don't support json_schema format via OpenRouter proxy. Therefore I reverted to parsing raw text responses with Zod validation.
Security Considerations
Session ID Exposure:
Session IDs are currently unprotected UUIDs transmitted in HTTP headers. Anyone with a session ID can:
- Read all todos in that session
- Create, modify, or delete todos
- Access AI analysis results
For Production, Consider:
- Authentication - Add proper auth (JWT, OAuth) before session creation
- Session Encryption - Use signed/encrypted session tokens
- Rate Limiting - Prevent session ID enumeration attacks
- Session Expiry - Current 1-hour TTL helps but isn't sufficient
- LLM Usage limits - Prevent abuse of AI analysis tool
Time Spent
Total: ~3.5 hours
Most of the time was spent on file-structure and testing of the functionality. Claude Code was used for brainstorming the multi-node session management approach, cleaning up syntax, documenting the code, making it easy to setup and writing this README.
License
MIT











