Todo MCP Server
by bxxf
README.md
# 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:**
- [Bun](https://bun.sh) (v1.0+) - JavaScript runtime
- [Docker](https://www.docker.com/get-started) - For Redis and multi-node deployment
**OpenRouter API Key:**
You need an OpenRouter API key to run the AI analysis feature.
1. Get your API key from [openrouter.ai/keys](https://openrouter.ai/keys)
2. Copy the example environment file:
```bash
cp .env.example .env
```
3. Edit `.env` and add your API key:
```bash
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.
```bash
# 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:
```bash
# 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:
```bash
# 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:
```bash
# 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:**
1. Copy the example config: `cp .mcp.json.example .mcp.json`
2. Edit `.mcp.json` and update the absolute path and OpenRouter API key
3. Start Redis: `docker run -d -p 6379:6379 redis:7-alpine`
4. 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.
```json
{
"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
```bash
# 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
```bash
# 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 `sessionId` to tool handlers
- Stateless SDK transport (`WebStandardStreamableHTTPServerTransport`) with no `sessionIdGenerator`
- Auto-session creation on first request with `Mcp-Session-Id` header 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:**
1. **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
2. **Modify SDK source** - Fork the SDK to add session parameter support
- ❌ Maintenance burden
- ❌ Out of sync with upstream
- ✅ Cleaner API
3. **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:**
1. **Authentication** - Add proper auth (JWT, OAuth) before session creation
2. **Session Encryption** - Use signed/encrypted session tokens
3. **Rate Limiting** - Prevent session ID enumeration attacks
4. **Session Expiry** - Current 1-hour TTL helps but isn't sufficient
5. **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
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues