Skip to main content
Glama
bxxf

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