docs: specify request-scoped multi-user architecture and ADR-012
- Add Section 3 in docs/ARCHITECTURE.md detailing multi-user concurrency and RequestContext - Add ADR-012 in docs/DECISIONS.md establishing RequestContext pattern over global state - Update AGENTS.md mandating stateless services and context propagation - Update docs/TODO.md with src/context.ts implementation task
This commit is contained in:
@@ -46,6 +46,7 @@ Welcome to the `openproject-mcp` project. This document defines operating princi
|
|||||||
### Code Style
|
### Code Style
|
||||||
- **TypeScript**: Strict type checking enabled (`strict: true` in `tsconfig.json`). Avoid `any` - use `unknown` with type guards or explicit Zod schemas.
|
- **TypeScript**: Strict type checking enabled (`strict: true` in `tsconfig.json`). Avoid `any` - use `unknown` with type guards or explicit Zod schemas.
|
||||||
- **Modularity**: Separate transport/MCP protocol concerns from OpenProject domain logic and HTTP client handling.
|
- **Modularity**: Separate transport/MCP protocol concerns from OpenProject domain logic and HTTP client handling.
|
||||||
|
- **Stateless Services & Request Scoping**: Never store API tokens or client instances in global mutable variables. Tool handlers and domain services must retrieve the user's isolated client via `RequestContext` (`AsyncLocalStorage`) to guarantee multi-user concurrency and credential isolation.
|
||||||
- **Naming Conventions**:
|
- **Naming Conventions**:
|
||||||
- Files and directories: `kebab-case` (e.g., `openproject-client.ts`, `work-packages.ts`).
|
- Files and directories: `kebab-case` (e.g., `openproject-client.ts`, `work-packages.ts`).
|
||||||
- Types and Interfaces: `PascalCase` (e.g., `OpenProjectConfig`, `WorkPackageResource`).
|
- Types and Interfaces: `PascalCase` (e.g., `OpenProjectConfig`, `WorkPackageResource`).
|
||||||
|
|||||||
+84
-36
@@ -6,38 +6,42 @@
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
subgraph Client ["MCP Client"]
|
subgraph Clients ["MCP Clients (Multi-User)"]
|
||||||
Agent["LLM Agent / Assistant"]
|
UserA["User A Client\n(Key A)"]
|
||||||
|
UserB["User B Client\n(Key B)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Server ["openproject-mcp Server"]
|
subgraph Transports ["Transport Layer"]
|
||||||
Transport["Stdio Transport"]
|
Stdio["Stdio Transport\n(Personal Process Isolation)"]
|
||||||
Router["Tool & Resource Router"]
|
SSE["SSE / HTTP Transport\n(Shared Central Daemon)"]
|
||||||
|
end
|
||||||
|
|
||||||
subgraph DomainServices ["Domain Services"]
|
subgraph ContextLayer ["Session & Request Context Provider"]
|
||||||
ProjSvc["Projects Service"]
|
ALS["AsyncLocalStorage /\nSession Context Resolver"]
|
||||||
WpSvc["Work Packages Service"]
|
CtxA["RequestContext A\n(Client A, Key A)"]
|
||||||
QuerySvc["Queries Service"]
|
CtxB["RequestContext B\n(Client B, Key B)"]
|
||||||
MetaSvc["Metadata & Taxonomies"]
|
end
|
||||||
end
|
|
||||||
|
|
||||||
ClientCore["OpenProject API Client"]
|
subgraph DomainServices ["Domain Services (Stateless)"]
|
||||||
AuthHandler["Auth & Request Builder\n(Basic Auth: apikey:<token>)"]
|
ProjSvc["Projects Service"]
|
||||||
HalParser["HAL+JSON Parser & Normalizer"]
|
WpSvc["Work Packages Service"]
|
||||||
|
QuerySvc["Queries Service"]
|
||||||
|
MetaSvc["Metadata & Taxonomies"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Remote ["OpenProject Instance"]
|
subgraph Remote ["OpenProject Instance"]
|
||||||
ApiV3["REST API v3 (/api/v3)"]
|
ApiV3["OpenProject REST API v3\n(/api/v3 - RBAC Enforced)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
Agent <-->|MCP JSON-RPC (stdio)| Transport
|
UserA -->|stdio or HTTP headers| Stdio
|
||||||
Transport <--> Router
|
UserB -->|HTTP headers| SSE
|
||||||
Router --> DomainServices
|
Stdio --> ALS
|
||||||
DomainServices --> ClientCore
|
SSE --> ALS
|
||||||
ClientCore --> AuthHandler
|
ALS --> CtxA
|
||||||
ClientCore --> HalParser
|
ALS --> CtxB
|
||||||
AuthHandler -->|HTTPS Requests| ApiV3
|
CtxA --> DomainServices
|
||||||
ApiV3 -->|HAL+JSON Responses| HalParser
|
CtxB --> DomainServices
|
||||||
|
DomainServices -->|Authenticated API Requests| ApiV3
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -82,7 +86,48 @@ The server supports a dedicated read-only operating mode designed for auditing,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Tool Specifications (Browse & Query Scope)
|
## 3. Multi-User Concurrency & Request-Scoped Architecture
|
||||||
|
|
||||||
|
The server is architected from the ground up to support concurrent, multi-user operations safely without cross-user credential leakage, state bleed, or privilege escalation.
|
||||||
|
|
||||||
|
### 3.1. Core Concurrency Rules
|
||||||
|
1. **Zero Global Mutable State**: No global singleton `OpenProjectClient` or static API key variable exists.
|
||||||
|
2. **Stateless Domain Services**: All domain services (`ProjectsService`, `WorkPackagesService`, etc.) are stateless and operate exclusively on the `RequestContext` resolved for the active execution.
|
||||||
|
3. **RequestContext Interface**:
|
||||||
|
```typescript
|
||||||
|
export interface RequestContext {
|
||||||
|
client: OpenProjectClient;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
sessionId?: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Context Propagation via AsyncLocalStorage**:
|
||||||
|
In Bun/TypeScript, request context is propagated down asynchronous call trees using `AsyncLocalStorage<RequestContext>`. Tool handlers access `getRequestContext()` to obtain their session's isolated client and read-only flags cleanly.
|
||||||
|
|
||||||
|
### 3.2. Supported Deployment Models
|
||||||
|
|
||||||
|
#### Model A: Local Personal Desktop (Stdio Transport - Default)
|
||||||
|
- **Deployment**: The MCP client (Claude Desktop, Cursor, local CLI) spawns `openproject-mcp` as a dedicated child process communicating via standard input/output.
|
||||||
|
- **Isolation**: OS-level process and memory boundary.
|
||||||
|
- **Credentials**: Passed via process environment variables or local MCP client configuration.
|
||||||
|
- **Security**: Complete isolation. Zero network ports exposed.
|
||||||
|
|
||||||
|
#### Model B: Centralized Shared Server (SSE / HTTP Transport)
|
||||||
|
- **Deployment**: A single shared `openproject-mcp` daemon runs centrally for a team or cluster.
|
||||||
|
- **Isolation**: Connection and request-level isolation.
|
||||||
|
- **Credential Delivery via Headers**:
|
||||||
|
- Each connecting client supplies their OpenProject API token via standard HTTP headers during connection handshake and request dispatch:
|
||||||
|
- `Authorization: Basic base64(apikey:<USER_API_KEY>)`, or
|
||||||
|
- `X-OpenProject-API-Key: <USER_API_KEY>`
|
||||||
|
- Optional: `X-OpenProject-Read-Only: true`
|
||||||
|
- The transport layer extracts the credentials, instantiates an ephemeral scoped `OpenProjectClient`, and executes tool calls inside `requestContextStorage.run(context, ...)`.
|
||||||
|
- **RBAC Enforcement**: All calls execute against OpenProject REST API v3 using that specific user's token. OpenProject's internal Role-Based Access Control guarantees users only retrieve projects and work packages they have permission to access.
|
||||||
|
- **Lifecycle**: Ephemeral client instances and tokens reside only in active session memory and are garbage collected upon connection close.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tool Specifications (Browse & Query Scope)
|
||||||
|
|
||||||
| MCP Tool Name | Description | Key Parameters |
|
| MCP Tool Name | Description | Key Parameters |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
@@ -99,9 +144,9 @@ The server supports a dedicated read-only operating mode designed for auditing,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. OpenProject API v3 Specifics
|
## 5. OpenProject API v3 Specifics
|
||||||
|
|
||||||
### 4.1. Authentication Details
|
### 5.1. Authentication Details
|
||||||
OpenProject API v3 utilizes HTTP Basic Authentication for API tokens:
|
OpenProject API v3 utilizes HTTP Basic Authentication for API tokens:
|
||||||
```http
|
```http
|
||||||
GET /api/v3/projects HTTP/1.1
|
GET /api/v3/projects HTTP/1.1
|
||||||
@@ -111,10 +156,10 @@ Accept: application/hal+json
|
|||||||
```
|
```
|
||||||
Where `YXBpa2V5OnlvdXItb3BlbnByb2plY3QtYXBpLWtleQ==` is the Base64 encoding of `apikey:<your-api-key>`.
|
Where `YXBpa2V5OnlvdXItb3BlbnByb2plY3QtYXBpLWtleQ==` is the Base64 encoding of `apikey:<your-api-key>`.
|
||||||
|
|
||||||
### 4.2. HAL+JSON Handling
|
### 5.2. HAL+JSON Handling
|
||||||
API responses contain `_links` linking to related resources (parent project, author, assignee, status, type). The client layer normalizes these into human-readable labels and numeric IDs so the LLM agent can reason over them easily.
|
API responses contain `_links` linking to related resources (parent project, author, assignee, status, type). The client layer normalizes these into human-readable labels and numeric IDs so the LLM agent can reason over them easily.
|
||||||
|
|
||||||
### 4.3. Filter Syntax Translation
|
### 5.3. Filter Syntax Translation
|
||||||
OpenProject filters are passed via a URL-encoded JSON string array. Example:
|
OpenProject filters are passed via a URL-encoded JSON string array. Example:
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
@@ -126,26 +171,29 @@ The client layer provides helper builders so tools can accept friendly parameter
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Security & Isolation
|
## 6. Security & Isolation
|
||||||
|
|
||||||
1. **Token Protection**: Credentials are read from environment variables or standard config files. Tokens are never echoed in logs or responses.
|
1. **Token Protection**: Credentials are read from environment variables or per-connection HTTP headers. Tokens are never echoed in logs or tool payloads.
|
||||||
2. **Permission Boundary**: The MCP server operates with the exact permission scope of the provided API key. No elevated access is granted beyond what the user possesses in OpenProject.
|
2. **Permission Boundary**: The MCP server operates strictly within the permission scope of the provided API key. No elevated access is granted beyond what the user possesses in OpenProject.
|
||||||
3. **Input Sanitization**: All arguments from the LLM are validated via Zod schemas prior to making network requests, preventing request-smuggling or path traversal.
|
3. **Input Sanitization**: All arguments from the LLM are validated via Zod schemas prior to making network requests, preventing request-smuggling or path traversal.
|
||||||
4. **Transport Isolation**: The default stdio transport communicates only through standard input/output with the host client process, with no exposed network ports.
|
4. **Transport Isolation**: The default stdio transport communicates only through standard input/output with the host client process, with no exposed network ports.
|
||||||
5. **Read-Only Enforcement**: Configurable read-only mode (`OPENPROJECT_READ_ONLY=true`) guarantees zero write side-effects on the OpenProject instance when operating in exploratory or untrusted agent sessions.
|
5. **Read-Only Enforcement**: Configurable read-only mode (`OPENPROJECT_READ_ONLY=true`) guarantees zero write side-effects on the OpenProject instance when operating in exploratory or untrusted agent sessions.
|
||||||
|
6. **Multi-Tenant Memory Isolation**: Ephemeral `RequestContext` instances isolated via `AsyncLocalStorage` guarantee zero token bleed or cross-user contamination in concurrent shared server environments.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Project Directory Structure
|
## 7. Project Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
openproject-mcp/
|
openproject-mcp/
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── ARCHITECTURE.md # System architecture and specifications
|
│ ├── ARCHITECTURE.md # System architecture and specifications
|
||||||
│ └── DECISIONS.md # Architecture Decision Records (ADR)
|
│ ├── DECISIONS.md # Architecture Decision Records (ADR)
|
||||||
|
│ └── TODO.md # Task tracking and roadmap
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── index.ts # CLI entry point and startup
|
│ ├── index.ts # CLI entry point and startup
|
||||||
│ ├── server.ts # MCP server instance & tool registration
|
│ ├── server.ts # MCP server instance & tool registration
|
||||||
|
│ ├── context.ts # RequestContext & AsyncLocalStorage
|
||||||
│ ├── client/
|
│ ├── client/
|
||||||
│ │ ├── api-client.ts # OpenProject REST API v3 HTTP client
|
│ │ ├── api-client.ts # OpenProject REST API v3 HTTP client
|
||||||
│ │ ├── hal-parser.ts # HAL+JSON parsing and transformation
|
│ │ ├── hal-parser.ts # HAL+JSON parsing and transformation
|
||||||
@@ -174,9 +222,9 @@ openproject-mcp/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Future Roadmap
|
## 8. Future Roadmap
|
||||||
|
|
||||||
- **Phase 1 (Current)**: Read/browse capability for projects, work packages, queries, and taxonomies.
|
- **Phase 1 (Current)**: Read/browse capability for projects, work packages, queries, and taxonomies with request-scoped context.
|
||||||
- **Phase 2**: Mutating operations (create/update work packages, add comments, log time).
|
- **Phase 2**: Mutating operations (create/update work packages, add comments, log time).
|
||||||
- **Phase 3**: Attachment inspection and download resources.
|
- **Phase 3**: Attachment inspection and download resources.
|
||||||
- **Phase 4**: SSE / Stream transport for remote server deployments and multi-user environments.
|
- **Phase 4**: SSE / Stream transport for remote multi-user server deployments.
|
||||||
|
|||||||
@@ -258,5 +258,46 @@ Implement an explicit, configurable **Read-Only Mode**:
|
|||||||
- **Negative**:
|
- **Negative**:
|
||||||
- Tools must explicitly declare whether they are mutating or read-only during registration.
|
- Tools must explicitly declare whether they are mutating or read-only during registration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADR-012: Request-Scoped Context Architecture for Multi-User Concurrency & Security
|
||||||
|
|
||||||
|
### Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
### Context
|
||||||
|
We want multiple users to interact with OpenProject via the MCP server concurrently, each using their own personal API key and receiving responses according to their own OpenProject permissions (RBAC).
|
||||||
|
|
||||||
|
Two architectural options were considered:
|
||||||
|
1. **Global Singleton Client**: Easy to write for single-user CLI scripts, but completely broken for concurrent users (race conditions, token overwriting, cross-user privilege escalation, and token leakage).
|
||||||
|
2. **Request-Scoped Context Pattern (`RequestContext` + `AsyncLocalStorage`)**: Every incoming request/connection carries or resolves an isolated `RequestContext` containing an ephemeral `OpenProjectClient` configured with that user's specific API credentials.
|
||||||
|
|
||||||
|
### Decision
|
||||||
|
Adopt the **Request-Scoped Context Architecture**:
|
||||||
|
1. **No Global State**: No singleton `OpenProjectClient` instance or mutable global API token will be maintained.
|
||||||
|
2. **Context Interface**:
|
||||||
|
```typescript
|
||||||
|
export interface RequestContext {
|
||||||
|
client: OpenProjectClient;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
sessionId?: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **Dual-Transport Compatibility**:
|
||||||
|
- **Local Stdio Mode**: The process context initializes a single `RequestContext` from local environment variables or CLI flags, isolated at the OS process boundary.
|
||||||
|
- **Shared Server (SSE / HTTP) Mode**: The connection handler extracts the user's API key from HTTP request headers (`Authorization: Basic ...` or `X-OpenProject-API-Key`), builds an ephemeral `RequestContext`, and wraps tool execution inside `requestContextStorage.run(context, fn)`.
|
||||||
|
4. **Stateless Services**: All domain services (`ProjectsService`, `WorkPackagesService`, etc.) are stateless function collections or classes that resolve the current user's client from `getRequestContext()`.
|
||||||
|
|
||||||
|
### Consequences
|
||||||
|
- **Positive**:
|
||||||
|
- Eliminates cross-user token leaks and race conditions in concurrent multi-user environments.
|
||||||
|
- Automatically respects OpenProject's per-user Role-Based Access Control (User A only sees User A's data).
|
||||||
|
- Unifies local desktop (`stdio`) and remote shared (`sse`) implementations under a single domain codebase.
|
||||||
|
- Tokens and client instances are ephemeral and garbage-collected with the request lifecycle.
|
||||||
|
- **Negative**:
|
||||||
|
- Tool handlers must access their client via `getRequestContext()` rather than directly importing a static instance.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
- [ ] **Task 1: OpenProject REST API v3 Client Layer**
|
- [ ] **Task 1: OpenProject REST API v3 Client Layer**
|
||||||
- [ ] Implement `src/config/index.ts` to parse and validate `OPENPROJECT_BASE_URL`, `OPENPROJECT_API_KEY`, and `OPENPROJECT_READ_ONLY` (or `--read-only`) via Zod.
|
- [ ] Implement `src/config/index.ts` to parse and validate `OPENPROJECT_BASE_URL`, `OPENPROJECT_API_KEY`, and `OPENPROJECT_READ_ONLY` (or `--read-only`) via Zod.
|
||||||
|
- [ ] Implement `src/context.ts` providing `RequestContext` interface and `AsyncLocalStorage` scoping (`getRequestContext`, `runWithContext`) for concurrent multi-user safety.
|
||||||
- [ ] Implement `src/client/api-client.ts` with native `fetch`, HTTP Basic Auth injection, error normalization, and rate/timeout handling.
|
- [ ] Implement `src/client/api-client.ts` with native `fetch`, HTTP Basic Auth injection, error normalization, and rate/timeout handling.
|
||||||
- [ ] Implement `src/client/hal-parser.ts` to unpack HAL+JSON (`_links`, `_embedded`) into clean, token-efficient JSON models.
|
- [ ] Implement `src/client/hal-parser.ts` to unpack HAL+JSON (`_links`, `_embedded`) into clean, token-efficient JSON models.
|
||||||
- [ ] Implement `src/client/filter-builder.ts` to translate LLM-friendly filter arguments into OpenProject's JSON filter syntax.
|
- [ ] Implement `src/client/filter-builder.ts` to translate LLM-friendly filter arguments into OpenProject's JSON filter syntax.
|
||||||
|
|||||||
Reference in New Issue
Block a user