Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,38 @@ npm run dev:server-demo
npm run dev:client-demo
```

## Intent Layer Navigation

Read the relevant Intent Node before working in that area:

- `src/types/CLAUDE.md` - Type definitions and contracts
- `src/core/CLAUDE.md` - ServerOrchestrator, DynamicToolManager, ToolRegistry
- `src/mode/CLAUDE.md` - ModuleResolver, validation utilities
- `src/server/CLAUDE.md` - createMcpServer, createPermissionBasedMcpServer
- `src/http/CLAUDE.md` - FastifyTransport, endpoints, SSE
- `src/session/CLAUDE.md` - SessionContextResolver, ClientResourceCache
- `src/permissions/CLAUDE.md` - PermissionResolver, access control

### Maintaining the Intent Layer

**AI agents working in this codebase must keep Intent Nodes up to date.** When you:

- **Add a new invariant** → Document it in the relevant Intent Node
- **Change component behavior** → Update the affected Intent Node's description
- **Add new components** → Add them to the Key Components section
- **Discover an anti-pattern** → Add it to the Anti-patterns section
- **Create a new module** → Create a corresponding `CLAUDE.md` Intent Node

Intent Nodes should remain concise (~100 lines max). Focus on what an agent needs to know to work safely in that area.

## Critical Invariants

1. **All tools → ToolRegistry** - Collision detection happens here only
2. **Disable ≠ Unregister** - MCP SDK limitation; disabled tools remain callable
3. **STATIC + sessionContext** - Session context ignored in STATIC mode
4. **Fail-secure** - Invalid inputs return empty objects, not errors
5. **Silent module failures** - Toolsets activate with partial tools if loaders fail

## Architecture

### Core Components
Expand Down
86 changes: 86 additions & 0 deletions src/core/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Core Module

## Purpose

Central orchestration layer that wires together all components. Manages toolset lifecycle, tool registration, and server initialization.

## Key Components

**ServerOrchestrator** (`ServerOrchestrator.ts`)
- Entry point combining ModuleResolver, DynamicToolManager, ToolRegistry
- `ensureReady()`: Async - waits for initialization, throws stored errors
- `isReady()`: Non-throwing alternative for health checks
- `getMode()`: Returns resolved DYNAMIC or STATIC mode
- `getManager()`: Access to DynamicToolManager

**DynamicToolManager** (`DynamicToolManager.ts`)
- `enableToolset(name, skipNotification?)`: Full validation + registration flow
- `disableToolset(name)`: State-only operation (see invariant #2)
- `enableToolsets(names)`: Batch enable with single notification
- `checkExposurePolicy(name)`: Validates against allowlist/denylist/maxActive
- `getStatus()`: Returns available/active toolsets, tools list, toolset→tools map

**ToolRegistry** (`ToolRegistry.ts`)
- `getSafeName(toolsetKey, toolName)`: Applies namespacing if enabled
- `has(name)`: Collision detection
- `add(name)` / `addForToolset(toolsetKey, name)`: Registration with collision check
- `mapAndValidate(toolsetKey, tools)`: Transforms tools with safe names, checks collisions

## Invariants

1. **All tools go through ToolRegistry** - Collision detection happens here only
2. **Disable is state-only** - MCP SDK cannot unregister tools; disabled toolsets' tools remain callable
3. **Notifications may fail silently** - "Not connected" errors are expected and swallowed
4. **Namespacing applied BEFORE collision check** - `mapAndValidate()` generates safe names first
5. **Toolset added to activeToolsets AFTER registration** - Partial failures leave toolset inactive

## Meta-tools Registration

Located in `src/meta/registerMetaTools.ts` (called by ServerOrchestrator):

**DYNAMIC mode only:**
- `enable_toolset` / `disable_toolset` - Runtime toolset management
- `list_toolsets` / `describe_toolset` - Discovery

**Both modes:**
- `list_tools` - List registered tool names

## Anti-patterns

- Bypassing ToolRegistry for tool registration (causes collision issues)
- Expecting disable to unregister tools from MCP (it can't)
- Throwing on notification failures (they're expected in SSE disconnect)

## Enable Toolset Flow

```
enableToolset(name)
ModuleResolver.validateToolsetName(name)
checkExposurePolicy(name) → fail fast if denied
ModuleResolver.resolveToolsForToolsets([name], context)
ToolRegistry.mapAndValidate(name, tools) → apply namespacing
For each tool: registerSingleTool(tool, name)
activeToolsets.add(name)
notifyToolsChanged() (unless skipNotification)
```

## Dependencies

- Imports: `src/types`, `src/mode`, `src/meta`
- Used by: `src/server/*`, `src/http/*`

## See Also

- `src/mode/CLAUDE.md` - How tools are resolved
- `src/server/CLAUDE.md` - How orchestrator is created
- `src/types/CLAUDE.md` - ExposurePolicy, ToolingErrorCode

---
*Keep this Intent Node updated when modifying core orchestration. See root CLAUDE.md for maintenance guidelines.*
105 changes: 105 additions & 0 deletions src/http/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# HTTP Module

## Purpose

Provides Fastify-based HTTP transport for the MCP protocol. Handles SSE streams, JSON-RPC requests, and per-client server management.

## Key Components

**FastifyTransport** (`FastifyTransport.ts`)
- Main HTTP transport using Fastify
- Per-client bundles via ClientResourceCache
- Optional SessionContextResolver for context differentiation

**Custom Endpoints** (`customEndpoints.ts`, `endpointRegistration.ts`)
- Type-safe endpoint definitions with Zod schemas
- `defineEndpoint()` / `definePermissionAwareEndpoint()` helpers
- Automatic request/response validation

## Endpoints

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/healthz` | Health check → `{ok: true}` |
| GET | `/tools` | Default manager status |
| GET | `/.well-known/mcp-config` | Config schema discovery |
| POST | `/mcp` | JSON-RPC requests (initialize, method calls) |
| GET | `/mcp` | SSE streaming for notifications |
| DELETE | `/mcp` | Session termination |

## Invariants

1. **Anonymous clients get `anon-` prefix** - Generated as `anon-${UUID}`, not cached
2. **Session created on POST /mcp initialize** - Tracked in `bundle.sessions` Map
3. **Cache key format** - `${clientId}:${contextHash}` when session context differs
4. **Reserved paths cannot be overridden** - `/mcp`, `/healthz`, `/tools`, `/.well-known/mcp-config`

## Request Extraction

```typescript
// Headers normalized to lowercase
headers: Record<string, string>

// Query params filtered to string values
query: Record<string, string>

// Client ID from header or auto-generated
clientId: headers['mcp-client-id'] ?? `anon-${uuid()}`
```

## Session Lifecycle

```
1. POST /mcp (initialize)
→ Create StreamableHTTPServerTransport
→ Generate session ID
→ Store in bundle.sessions

2. GET /mcp (streaming)
→ Require mcp-session-id header
→ Delegate to transport.handleRequest()
→ Maintain SSE connection

3. POST /mcp (subsequent)
→ Use existing session
→ Route JSON-RPC through transport

4. DELETE /mcp (cleanup)
→ Remove session from bundle
→ Close transport
```

## Custom Endpoint Registration

```typescript
// Standard endpoint
defineEndpoint({
path: '/my-endpoint',
method: 'POST',
body: z.object({ data: z.string() }),
handler: async (req, manager) => ({ result: 'ok' })
})

// Permission-aware (includes allowedToolsets, failedToolsets)
definePermissionAwareEndpoint({...})
```

## Anti-patterns

- Registering endpoints on reserved paths (throws)
- Assuming client IDs persist (anonymous regenerated each request)
- Blocking on SSE handlers (should be async)

## Dependencies

- Imports: `src/types`, `src/core`, `src/session`
- Used by: `src/server/createMcpServer`

## See Also

- `src/session/CLAUDE.md` - SessionContextResolver, ClientResourceCache
- `src/permissions/CLAUDE.md` - PermissionAwareFastifyTransport
- `src/server/CLAUDE.md` - How transport is configured

---
*Keep this Intent Node updated when modifying HTTP transport. See root CLAUDE.md for maintenance guidelines.*
61 changes: 61 additions & 0 deletions src/mode/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Mode Module

## Purpose

Handles toolset resolution, validation, and startup mode determination. Provides the bridge between toolset definitions and tool instances.

## Key Components

**ModuleResolver** (`ModuleResolver.ts`)
- `getAvailableToolsets()`: Returns all toolset keys from catalog
- `getToolsetDefinition(name)`: Retrieves single toolset definition
- `validateToolsetName(name)`: Returns `{isValid, sanitized, error}`
- `resolveToolsForToolsets(toolsets, context)`: Async - collects direct tools + module-loaded tools

**ToolsetValidator** (`ModeResolver.ts`)
- `resolveMode(env?, args?)`: Returns "DYNAMIC", "STATIC", or null
- `validateToolsetName(name, catalog)`: Validates against provided catalog
- `parseCommaSeparatedToolSets(input, catalog)`: Parses toolset strings
- `validateToolsetModules(toolsetNames, catalog)`: Returns modules for valid toolsets

## Invariants

1. **Module loaders fail silently** - Errors are caught, logged as warnings, and skipped; toolset activates with partial tools
2. **ToolsetValidator is in ModeResolver.ts** - Historical naming quirk: `ToolsetValidator.ts` just re-exports from `ModeResolver.ts`
3. **ModuleResolver stores catalog; ToolsetValidator takes it as parameter** - Different validation signatures
4. **Context passed to module loaders** - Enables dynamic tool generation based on runtime context

## Anti-patterns

- Throwing on invalid toolset names (return validation result instead)
- Assuming module loaders are synchronous (always await)
- Using ToolsetValidator.ts directly (it's just a re-export)

## Module Loading Flow

```
resolveToolsForToolsets([toolsetName], context)
For each toolset:
1. Collect direct tools from definition.tools[]
2. For each module in definition.modules[]:
- Look up loader in moduleLoaders map
- Call loader(context) - may be async
- Catch errors → warn and skip
- Append returned tools
Return flattened McpToolDefinition[]
```

## Dependencies

- Imports: `src/types`
- Used by: `src/core/ServerOrchestrator`, `src/core/DynamicToolManager`

## See Also

- `src/core/CLAUDE.md` - How resolved tools are registered
- `src/types/CLAUDE.md` - ModuleLoader type definition

---
*Keep this Intent Node updated when modifying mode resolution. See root CLAUDE.md for maintenance guidelines.*
91 changes: 91 additions & 0 deletions src/permissions/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Permissions Module

## Purpose

Provides per-client access control for toolsets. Supports header-based and config-based permission resolution with caching.

## Key Components

**PermissionResolver** (`PermissionResolver.ts`)
- `resolve(clientId, headers?)`: Returns allowed toolset names
- `invalidateCache(clientId)`: Clear specific client's cached permissions
- `clearCache()`: Clear all cached permissions

**validatePermissionConfig** (`validatePermissionConfig.ts`)
- Validates PermissionConfig structure
- Ensures source is "headers" or "config"
- For config source: requires staticMap or resolver

**createPermissionAwareBundle** (`createPermissionAwareBundle.ts`)
- Wraps bundle creation with permission enforcement
- Returns `{server, orchestrator, allowedToolsets, failedToolsets}`
- Throws if ALL requested toolsets fail (likely config error)

**PermissionAwareFastifyTransport** (`PermissionAwareFastifyTransport.ts`)
- HTTP transport with permission enforcement
- Per-client bundles via ClientResourceCache
- Anonymous clients (`anon-*`) not cached

## Invariants

1. **Permission cache has no TTL** - Must call `invalidateCache()` manually when permissions change
2. **Header lookup is case-insensitive** - Per RFC 7230
3. **Fail-secure resolution** - Invalid permissions return empty array, not error
4. **All toolsets fail = throw** - Partial success continues with warning

## Resolution Priority (config source)

```
1. resolver(clientId) → if provided and returns valid array
2. staticMap[clientId] → if provided and client exists
3. defaultPermissions → fallback
4. [] → if nothing else
```

## Caching Behavior

**PermissionResolver cache:**
- Keyed by clientId
- NO automatic expiration
- Manual invalidation required

**PermissionAwareFastifyTransport cache:**
- Keyed by clientId (non-anonymous only)
- LRU eviction with onEvict cleanup
- Closes all sessions in bundle on eviction

## Anti-patterns

- Expecting cache to auto-invalidate (it won't)
- Caching anonymous client bundles (they're excluded)
- Trusting client-provided permissions without config-based fallback

## Permission Flow

```
HTTP Request with mcp-client-id header
PermissionAwareFastifyTransport extracts client context
createPermissionAwareBundle(context)
PermissionResolver.resolve(clientId, headers)
Create STATIC mode server with allowed toolsets only
Return bundle (cached if non-anonymous)
```

## Dependencies

- Imports: `src/types`, `src/core`, `src/session`
- Used by: `src/server/createPermissionBasedMcpServer`

## See Also

- `src/http/CLAUDE.md` - FastifyTransport base class
- `src/server/CLAUDE.md` - createPermissionBasedMcpServer
- `src/types/CLAUDE.md` - PermissionConfig type

---
*Keep this Intent Node updated when modifying permissions. See root CLAUDE.md for maintenance guidelines.*
Loading