This guide shows you how to isolate state per workspace or organization when deploying the MCP server to multi-user environments. Use this when multiple Claude Code instances, Codex sessions, or API clients share a single server.
- Claude Prompts MCP server v1.7+
- Node.js >= 22 (required for native SQLite)
- Understanding of your deployment transport (STDIO vs streamable-HTTP)
The server extracts identity from MCP SDK transport metadata and uses it to isolate all persisted state (chain sessions, framework switches, gate system state) per workspace.
MCP SDK extra ──► RequestIdentityResolver ──► continuityScopeId ──► SQLite scoped queries
│ │
├─ authInfo.extra (OAuth claims) ├─ workspaceId (highest priority)
├─ requestInfo.headers (gateway headers) ├─ organizationId (fallback)
└─ launch defaults (CLI flags) └─ "default" (single-tenant fallback)
MCP request options ──► RequestIdentityResolver ──► clientProfile ──► handoff strategy
│ │
└─ options.client_profile (hint) ├─ task_tool_v1 (Claude Code)
├─ spawn_agent_v1 (Codex)
├─ gemini_subagent_v1 (Gemini)
├─ opencode_agent_v1 (OpenCode)
├─ cursor_agent_v1 (Cursor)
└─ neutral_v1 (unknown fallback)
All scope parameters are optional. Single-tenant deployments (STDIO with Claude Desktop) work unchanged with the default scope.
Use CLI flags to pin identity at launch time:
Client presets: claude-code, codex, gemini, opencode, cursor, unknown.
| Client preset | Handoff profile | Status | Handoff note |
|---|---|---|---|
claude-code |
task_tool_v1 |
canonical | Task tool flow |
codex |
spawn_agent_v1 |
canonical | spawn_agent preferred with runtime fallback guidance |
gemini |
gemini_subagent_v1 |
canonical | Gemini sub-agent capability guidance |
opencode |
opencode_agent_v1 |
canonical | OpenCode agent capability guidance |
cursor |
cursor_agent_v1 |
experimental/testing | Cursor handoff strategy is enabled but explicitly in testing |
unknown |
neutral_v1 |
canonical | Neutral fallback instructions |
# Pin to a specific workspace
node dist/index.js --transport=stdio --workspace-id=my-workspace
# Pin to an organization (workspace falls back to organization)
node dist/index.js --transport=stdio --organization-id=my-org
# Pin handoff client profile at launch (recommended for deterministic client routing)
node dist/index.js --transport=stdio --client=codex
# Locked mode: reject any per-request overrides
node dist/index.js --transport=stdio --workspace-id=my-workspace --identity-mode=lockedFor Claude Desktop, add the flags to your MCP server configuration:
{
"mcpServers": {
"claude-prompts": {
"command": "node",
"args": [
"/path/to/dist/index.js",
"--transport=stdio",
"--workspace-id=team-alpha",
"--client=claude-code"
]
}
}
}When deployed behind an API gateway (Kong, Envoy, NGINX) that injects identity headers:
node dist/index.js --transport=streamable-http --port=3000The server reads these headers automatically:
| Header | Maps To | Priority |
|---|---|---|
x-workspace-id, x-project-id |
workspaceId |
Highest |
x-organization-id, x-org-id |
organizationId |
Fallback |
x-actor-id, x-user-id |
actorId |
Audit only |
mcp-session-id |
transportSessionId |
Audit only |
OAuth token claims (via authInfo.extra) take priority over headers when both are present.
Add an identity section to your config.json:
{
"identity": {
"mode": "permissive",
"allowPerRequestOverride": true,
"launchDefaults": {
"organizationId": "my-org",
"workspaceId": "my-workspace"
}
}
}CLI launch flags (--workspace-id, --organization-id, --client) override identity.launchDefaults in memory for that server process.
| Mode | Behavior | Use Case |
|---|---|---|
permissive (default) |
HTTP: per-request claims override launch defaults. STDIO: launch defaults preferred, overrides allowed if allowPerRequestOverride: true |
Multi-tenant gateways, development |
locked |
Launch defaults always authoritative. Override attempts are logged and rejected. | Production single-workspace servers |
The server resolves identity through this hierarchy (first match wins):
| Source | Transport | When Used |
|---|---|---|
OAuth token claims (authInfo.extra) |
HTTP | Gateway forwards JWT claims |
Request headers (x-workspace-id) |
HTTP | Gateway injects headers |
Launch defaults (--workspace-id) |
Both | CLI flags or config |
Default ("default") |
Both | No identity provided |
The server resolves handoff client profile through this hierarchy (first match wins):
| Source | Transport | When Used |
|---|---|---|
Launch defaults (identity.launchDefaults.client*) |
Both | Authoritative deployment default |
Trusted request metadata (authInfo.extra, x-client-* headers) |
Mostly HTTP | Gateway/auth integration provides client profile |
Request hint (options.client_profile) |
Both | Caller passes protocol-level hint |
SDK heuristic (clientInfo.name/version) |
Both | MCP SDK exposes client metadata |
| Unknown fallback | Both | No profile signal available (neutral_v1) |
Most STDIO launches provide limited transport metadata, and clients may not expose stable identity fields to MCP servers consistently across versions.
In practice:
--clientat launch time is the most deterministic signal for handoff profile selection.- HTTP header signals (
x-client-*) are stronger in managed gateway deployments than in local desktop/CLI STDIO sessions. - SDK heuristics are fallback only and should not be treated as a strict contract.
-
Check resolved identity via system_control:
system_control(action: "whoami")Returns the resolved identity context including
continuityScopeId, source provenance, and policy mode. -
Test isolation by running the same chain from two different workspaces:
# Terminal 1: workspace-a node dist/index.js --transport=stdio --workspace-id=workspace-a # Terminal 2: workspace-b node dist/index.js --transport=stdio --workspace-id=workspace-b
Chain sessions, framework state, and gate state are fully isolated between workspaces.
-
Run tenant isolation tests:
cd server npm run test:integration -- --testPathPattern=tenantExpected: 14 tests passing (workspace continuity + tenant isolation).
| State | Isolated Per Scope | Notes |
|---|---|---|
| Chain sessions | Yes | Same chain_id runs independently across workspaces |
| Framework switches | Yes | Workspace A on CAGEERF, workspace B on ReACT |
| Gate system state | Yes | Enable/disable, health metrics, validation history |
| Argument history | Yes | Per-workspace argument tracking |
| Resource index | No | Shared file-based resources (prompts, gates, styles) |
Issue: All state lands in default scope despite passing --workspace-id
Fix: Verify the flag format. Both --workspace-id=value and --workspace-id value are accepted. Check system_control(action: "whoami") to see what the server resolved.
Issue: HTTP gateway headers are not picked up
Fix: Headers must be lowercase (x-workspace-id, not X-Workspace-ID). The server normalizes case, but ensure your gateway passes them on the HTTP request to the MCP endpoint.
Issue: Locked mode rejects requests
Fix: In locked mode, launchDefaults must be set. If no launch defaults are configured, the server falls back to default scope and logs a warning.
- Workspace Identity Migration -- naming migration history
- Client Integration -- per-client install snippets and verification steps
- Client Capabilities Reference -- presets, profile mapping, and limits
- Injection Control -- per-request methodology injection
- Architecture Overview -- pipeline stage reference