7.2 KiB
Design Specification: MCP Stdio Server Assembly & Integration
Date: 2026-09-06 Status: Approved Scope: Phase 1 Task 4 - MCP Stdio Server Assembly & Integration
1. Executive Summary
This specification defines the assembly, transport binding, request lifecycle management, read-only enforcement, and logging safeguards for the openproject-mcp stdio server.
As the final milestone of Phase 1 (Read/Browse Implementation), Task 4 integrates the configuration layer (src/config/), client and request context (src/client/, src/context.ts), and MCP tools (src/tools/) into an executable Model Context Protocol server communicating over standard input/output (StdioServerTransport) using @modelcontextprotocol/sdk.
2. Architecture & Design Principles
2.1 Server Factory Pattern (src/server.ts)
To ensure testability and separation from process lifecycle concerns, the server is instantiated via a factory function:
export interface OpenProjectMcpServer {
server: McpServer;
client: OpenProjectClient;
start: (transport?: Transport) => Promise<void>;
stop: () => Promise<void>;
}
export function createServer(config: AppConfig): OpenProjectMcpServer;
- Configuration Injection: Accepts validated
AppConfig(baseUrl,apiKey,readOnly). - Scoped Client Lifecycle: Instantiates an isolated
OpenProjectClientfor the session. - Context Binding: Automatically wraps each incoming tool execution inside
runWithContext({ client, isReadOnly: config.readOnly }, ...)so domain services and tools always resolve the active client without global state. - Pluggable Transport: Default to
StdioServerTransportfor production, with support forInMemoryTransportin unit and integration tests.
2.2 CLI Entrypoint (src/index.ts)
The CLI entrypoint:
- Validates environment variables and CLI arguments (
parseConfig(process.env, process.argv)). - Initializes the server via
createServer(config). - Binds graceful termination handlers (
SIGINT,SIGTERM). - Connects to
StdioServerTransportand starts listening.
+-------------------------------------------------------------+
| MCP Client Host |
| (Claude Desktop, Cursor, Antigravity) |
+-------------------------------------------------------------+
|
stdio (stdin / stdout)
v
+-------------------------------------------------------------+
| src/index.ts / src/server.ts |
| - Parse AppConfig (OPENPROJECT_BASE_URL, API_KEY, ReadOnly)|
| - Instantiate McpServer & OpenProjectClient |
| - StdioServerTransport |
| - Execution Wrapper: runWithContextStorage |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| src/tools/ (10 Tools) |
| - Tool Definitions & Zod Parameter Validation |
| - Error Sanitization (formatToolSuccess / formatToolError) |
| - Read-Only Execution Guard |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| src/services/ & src/client/ |
| - projects, work-packages, queries, metadata |
| - Native fetch to OpenProject API v3 (Basic Auth) |
+-------------------------------------------------------------+
2.3 Read-Only Mode Enforcement
Per ADR-005 and AGENTS.md requirements:
- Tool Manifest Filtering: When
config.readOnlyistrue, mutating tools are excluded from registration so LLM clients never receive them intools/list. - Runtime Execution Guard: In addition to manifest omission, any tool call executed while
isReadOnlyistruethat attempts a non-read-only action is rejected with error codeSERVER_READ_ONLY:{ "content": [ { "type": "text", "text": "Error [SERVER_READ_ONLY]: Operation rejected. OpenProject MCP server is running in read-only mode." } ], "isError": true } - Phase 1 Verification: All 10 Phase 1 tools are read-only (
readOnly: true), ensuring 100% availability in read-only mode while providing the enforcement harness for Phase 2 mutations.
2.4 Stdio Transport Integrity & Stderr Logging
In Model Context Protocol stdio transport:
stdoutis the communication channel for JSON-RPC messages. Any non-JSON text emitted tostdoutwill corrupt the transport and crash the MCP client.- All diagnostics, banners, startup messages, and errors must be directed to
stderr. createServerandindex.tsensure all startup logging usesconsole.error.
3. Interfaces & Contracts
3.1 src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { AppConfig } from "./config";
import { OpenProjectClient } from "./client/api-client";
export interface OpenProjectMcpServer {
server: McpServer;
client: OpenProjectClient;
start: (transport?: Transport) => Promise<void>;
stop: () => Promise<void>;
}
export function createServer(config: AppConfig): OpenProjectMcpServer;
3.2 Tool Execution Wrapping in src/tools/common.ts & src/tools/index.ts
Enhance registerAllTools and registerTool with wrapExecute:
export interface RegisterToolsOptions {
readOnly?: boolean;
tools?: AnyToolDefinition[];
wrapExecute?: (
fn: () => Promise<McpToolResponse>,
tool: AnyToolDefinition,
args: Record<string, unknown>
) => Promise<McpToolResponse>;
}
When wrapExecute is supplied:
- Checks if
isReadOnly && !tool.readOnly. If so, returnsformatToolError(new OpenProjectError("SERVER_READ_ONLY", "Operation rejected. OpenProject MCP server is running in read-only mode.")). - Executes the tool inside
runWithContext({ client, isReadOnly }, fn).
4. Testing Strategy
- End-to-End Server Tests (
tests/mcp-server.test.ts):- Verify server initialization with
createServer(config). - Connect client using
InMemoryTransport.createLinkedPair(). - Verify
client.listTools()returns all 10 Phase 1 tools. - Verify calling tools (
openproject_list_projects,openproject_get_work_package) viaclient.callTool()executes successfully against live OpenProject 17 container. - Verify invalid arguments return
isError: truewithout throwing unhandled exceptions.
- Verify server initialization with
- Read-Only Mode Tests (
tests/read-only.test.ts):- Verify config parsing with
OPENPROJECT_READ_ONLY=trueand--read-only. - Verify that all Phase 1 tools remain listed and functional.
- Verify that a synthetic mutating tool (
readOnly: false) is omitted fromclient.listTools(). - Verify that invoking a mutating tool triggers
SERVER_READ_ONLYexecution guard.
- Verify config parsing with
- Subprocess Stdio Verification:
- Spawn
bun run src/index.tsas a child process and verify startup banner onstderr.
- Spawn