8.1 KiB
Specification: Hosted Remote MCP Server (HTTP/SSE Transport) & Multi-Tenant Scoping
1. Overview & Objectives
Currently, openproject-mcp runs as a local subprocess communicating via standard input/output (stdio) transports. Every user must have Docker or Bun installed locally and run their own MCP process.
This specification introduces a Hosted Remote MCP Server capability. Organizations can deploy a single hosted instance of openproject-mcp (e.g. at https://mcp.example.com). Individual users and their AI clients (Claude Desktop, Cursor, web agents) connect directly to the hosted server over the standard Model Context Protocol HTTP/SSE (Server-Sent Events) transport, supplying their own personal OpenProject API keys.
Primary Objectives
- Hosted Multi-Tenant Remote Server: Provide an HTTP/SSE server implementation based on native
Bun.serveand@modelcontextprotocol/sdk. - Dual-Transport Entrypoint: Preserve existing local stdio operation by default; activate HTTP/SSE transport when
PORT(e.g.,PORT=3000) or--portis supplied. - Flexible Credential Scoping: Extract the user's OpenProject API key from incoming client requests via:
Authorization: Bearer <key>X-OpenProject-Api-Key: <key>- URL query parameter
?apiKey=<key>
- Single-Instance Multi-User Isolation: Target a shared OpenProject base URL (
OPENPROJECT_BASE_URL) configured on the server, while strictly isolating each client's requests insideRequestContext(AsyncLocalStorage) using their respective personal API key. - Unauthorized Rejection: Return clean HTTP 401 Unauthorized responses if a client connects without an API key.
- Docker Compose Deployment: Provide a
docker-compose.server.ymlfile andREADME.mdguide for hosting the server with health checks.
2. Architecture & Data Flow
+------------------------------------+ +------------------------------------+
| User A (Cursor / Claude Desktop) | | User B (Claude Desktop / Agent) |
| Headers: X-OpenProject-Api-Key: keyA| | URL: /sse?apiKey=keyB |
+-----------------+------------------+ +-----------------+------------------+
| (GET /sse) | (GET /sse)
+-----------------------+-------------------+
|
v
+------------------------------+
| Hosted openproject-mcp Server |
| (Bun.serve on PORT=3000) |
+--------------+---------------+
|
+-----------------+-----------------+
| Session A | Session B
| Client A (key: keyA) | Client B (key: keyB)
| McpServer A (runWithContext) | McpServer B (runWithContext)
+-----------------+-----------------+
|
v
+------------------------------+
| OpenProject 17 REST API v3 |
| (Shared OPENPROJECT_BASE_URL)|
+------------------------------+
2.1 HTTP Endpoints
-
GET /health:- Responds with HTTP 200:
{ "status": "ok", "mode": "remote-mcp", "openproject": "https://openproject.example.com", "readOnly": false } - Used by load balancers, Kubernetes probes, and Docker health checks.
- Responds with HTTP 200:
-
GET /sse:- Establishes a persistent Server-Sent Events (SSE) connection with the client.
- Extracts the API key from:
Authorization: Bearer <token>X-OpenProject-Api-Key: <token>- Query parameter
?apiKey=<token>
- If missing: Rejects immediately with HTTP 401 and JSON error message:
{ "error": "Missing OpenProject API key. Provide via Authorization header, X-OpenProject-Api-Key header, or ?apiKey= query parameter." } - If present:
- Generates a cryptographically random
sessionId. - Creates an isolated
OpenProjectClient({ baseUrl, apiKey }). - Creates an
SSEServerTransportbound to/messages?sessionId=${sessionId}. - Creates an
McpServerinstance with all tools registered and wrapped withrunWithContext. - Stores the session in
activeSessions: Map<string, SessionData>. - Streams the initial
endpointSSE event to the client.
- Generates a cryptographically random
-
POST /messages:- URL contains
?sessionId=<id>. - Looks up the session in
activeSessions. - If session not found: Returns HTTP 404 Not Found.
- Forwards the JSON-RPC request to the session transport's message handler, ensuring tool execution resolves the session's client via
runWithContext.
- URL contains
-
Session Eviction:
- On SSE connection close or error,
transport.oncloseevicts the session fromactiveSessionsto prevent memory leaks.
- On SSE connection close or error,
3. Configuration & CLI Flags
Configuration Extension (src/config/index.ts)
export interface AppConfig {
baseUrl: string;
apiKey?: string; // Optional when running in hosted HTTP mode
readOnly: boolean;
port?: number; // When present, launches HTTP/SSE server instead of stdio
host?: string; // Default: "0.0.0.0"
}
- When
portis defined (process.env.PORTor--port <num>):apiKeyis optional at server startup because individual connecting users supply their own keys.- Server starts in HTTP mode.
- When
portis undefined:- Server runs in stdio mode (requires
OPENPROJECT_API_KEYin environment).
- Server runs in stdio mode (requires
4. Docker Compose Setup (docker-compose.server.yml)
A ready-to-run Docker Compose file for deploying the hosted MCP server:
version: "3.8"
services:
openproject-mcp:
image: ghcr.io/stereotypicalcat/openproject-mcp:latest
container_name: openproject-mcp-hosted
restart: unless-stopped
ports:
- "3000:3000"
environment:
- OPENPROJECT_BASE_URL=https://openproject.example.com
- PORT=3000
- OPENPROJECT_READ_ONLY=false
healthcheck:
test: ["CMD-SHELL", "bun -e 'fetch(\"http://localhost:3000/health\").then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))'"]
interval: 30s
timeout: 5s
retries: 3
5. Client Configuration Examples
Cursor
Add to .cursor/mcp.json (or Cursor Settings > Features > MCP):
{
"mcpServers": {
"openproject": {
"url": "https://mcp.example.com/sse",
"headers": {
"X-OpenProject-Api-Key": "your-personal-api-key"
}
}
}
}
Or via URL query parameter:
{
"mcpServers": {
"openproject": {
"url": "https://mcp.example.com/sse?apiKey=your-personal-api-key"
}
}
}
Claude Desktop
In clients that connect to remote SSE servers via standard proxy or direct URL:
{
"mcpServers": {
"openproject": {
"url": "https://mcp.example.com/sse?apiKey=your-personal-api-key"
}
}
}
6. Verification & Test Plan
- Unit & Protocol Tests (
tests/http-server.test.ts):GET /healthreturns HTTP 200 with status info.GET /ssewithout credentials returns HTTP 401.GET /ssewithAuthorization: Bearer <key>succeeds and returns SSE stream.GET /ssewithX-OpenProject-Api-Key: <key>succeeds.GET /ssewith?apiKey=<key>succeeds.- Concurrent clients with different API keys execute tools with their own respective credentials without bleed.
- Disconnecting client evicts session from memory.
- CLI & Entrypoint Tests:
PORT=3000boots the HTTP server.- CLI flags
--port 3000boots the HTTP server.
- Docker Compose Validation:
- Validate
docker-compose.server.ymlconfiguration and verify container runs in HTTP mode.
- Validate
- Documentation:
- Record ADR-016 in
docs/DECISIONS.md. - Update
README.mdwith Hosted Remote Server and Docker Compose deployment instructions.
- Record ADR-016 in