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
113 changes: 62 additions & 51 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,82 @@
# Using Toolception with Agents/LLMs
# Toolception — Intent Layer Root

## Capabilities by mode
## What This Is

- Dynamic mode:
- Meta-tools available: `enable_toolset`, `disable_toolset`, `list_toolsets`, `describe_toolset`, and `list_tools`.
- Tools may change at runtime; server advertises `tools.listChanged` capability.
- Static mode:
- Meta-tools available: `list_tools` only. Do not attempt to enable/disable toolsets.
Dynamic MCP server toolkit for runtime toolset management. Groups tools into toolsets, exposes only what's needed — reducing prompt/tool surface area for LLMs.

## Recommended agent flow
## Architecture

1. Discover current tools
- Always call `list_tools()` first. Response includes `tools` and `toolsetToTools`.
2. (Dynamic only) Discover available toolsets
- If `list_toolsets` is present, call it to see all toolsets, their definitions, and which are active.
3. (Dynamic only) Enable toolsets on demand
- Call `enable_toolset({ name })` for a specific toolset. Handle failures:
- Not allowed/denied by policy
- Already active
- Max active toolsets exceeded
4. Invoke task-specific tools using namespaced names (e.g., `search.find`).
5. (Optional) Disable toolsets no longer needed via `disable_toolset({ name })` (state-only).
```
src/
├── types/ # Contracts (leaf — no deps)
├── core/ # ServerOrchestrator, DynamicToolManager, ToolRegistry
├── mode/ # ModuleResolver, toolset validation
├── meta/ # Meta-tool registration (enable/disable/list)
├── server/ # createMcpServer, createPermissionBasedMcpServer
├── http/ # FastifyTransport, custom endpoints
├── session/ # SessionContextResolver, ClientResourceCache
├── permissions/ # PermissionResolver, PermissionAwareFastifyTransport
└── errors/ # ToolingError (18 LOC, thin wrapper — no Intent Node)
```

## Tool naming
### Two Server Modes

- Tools are namespaced by toolset (e.g., `search.find`) to avoid collisions.
- Namespace policy may be customized; default is ON.
| | DYNAMIC | STATIC |
|---|---|---|
| Toolsets | Enabled at runtime via meta-tools | Pre-loaded at startup |
| Server instances | Per-client | Shared singleton |
| Meta-tools | All 5 registered | `list_tools` only |
| Use case | Task-specific, lazy loading | Fixed pipelines |

## Error handling cues
### Permission-Based Variant

Meta-tool return formats:
- `enable_toolset` and `disable_toolset` return `{ success: boolean, message: string }`
- `list_tools` returns `{ tools: string[], toolsetToTools: Record<string, string[]> }`
- `list_toolsets` returns `{ toolsets: Array<{ key, active, definition, tools }> }`
- `describe_toolset` returns `{ key, active, definition, tools }` or `{ error: string }` if unknown
`createPermissionBasedMcpServer` — per-client STATIC servers with access control. Permissions resolved from headers or server config. No meta-tools.

For `enable_toolset`/`disable_toolset`, read `message` to adapt decisions (e.g., policy denial, already active, limits exceeded).
## Code Style

## HTTP endpoints
1. **No non-null assertions (`!`)** — Guard before use or use conditional builder calls. Never use `!` to silence the compiler.
2. **No `as any` in production code** — Permitted only for defensive runtime guards and SDK boundary mismatches. Every `as any` should have a comment justifying it.
3. **Named functions over anonymous callbacks** — Extract inline closures longer than ~5 lines into named functions with JSDoc.
4. **Builder pattern: conditional calls for optional fields** — Call builder methods conditionally (`if (value) { builder.method(value); }`) rather than asserting.
5. **Prefer `builder()` over raw constructors** — When a class exposes a builder, use it.
6. **Named intermediate variables** — Assign computed values to descriptively-named `const` variables before passing them onward.

### Built-in MCP endpoints
## Critical Invariants

- `GET /healthz` - Health check
- `GET /tools` - List available toolsets and tools
- `POST /mcp` - MCP JSON-RPC requests (accepts `?config=` query parameter for session context)
- `GET /mcp` - Server-sent events stream (accepts `?config=` query parameter for session context)
- `DELETE /mcp` - Close session
- `GET /.well-known/mcp-config` - Configuration schema
These are cross-cutting; see leaf Intent Nodes for module-specific invariants.

### Custom HTTP endpoints
1. **All tools → ToolRegistry** — Collision detection happens here only
2. **`mcp-client-id` required for /mcp** — POST, GET, DELETE all reject without header (400)
3. **Fail-secure** — Invalid inputs return empty objects/arrays, not errors
4. **Silent module failures** — Toolsets activate with partial tools if loaders fail
5. **Disable is state-only** — MCP SDK has no tool unregister; see `src/core/AGENTS.md`

Servers may expose custom REST-like endpoints alongside MCP protocol endpoints. These are defined by the server implementer and provide direct HTTP access to functionality.
## Module Index

- Custom endpoints use standard HTTP methods (GET, POST, PUT, DELETE, PATCH)
- Request/response validation via Zod schemas
- Access to client ID via `mcp-client-id` header
- Permission-aware endpoints receive client's allowed toolsets
- Standard error format with `VALIDATION_ERROR`, `INTERNAL_ERROR`, or `RESPONSE_VALIDATION_ERROR` codes
Read the relevant Intent Node before working in that area:

Check `GET /tools` or server documentation to discover available custom endpoints.
| Module | Intent Node | Covers |
|--------|-------------|--------|
| Types | `src/types/AGENTS.md` | Interfaces, contracts, error codes |
| Core | `src/core/AGENTS.md` | ServerOrchestrator, DynamicToolManager, ToolRegistry |
| Meta | `src/meta/AGENTS.md` | Meta-tool registration, `_meta` reserved key |
| Mode | `src/mode/AGENTS.md` | ModuleResolver, toolset validation |
| Server | `src/server/AGENTS.md` | createMcpServer, createPermissionBasedMcpServer |
| HTTP | `src/http/AGENTS.md` | FastifyTransport, endpoints, SSE, custom endpoints |
| Session | `src/session/AGENTS.md` | SessionContextResolver, ClientResourceCache |
| Permissions | `src/permissions/AGENTS.md` | PermissionResolver, PermissionAwareFastifyTransport |

### Headers
## Maintaining Intent Nodes

- `mcp-client-id`: Client identifier (reuse for per-client sessions)
- `mcp-session-id`: Session identifier (managed by MCP transport after initialize)
- `mcp-toolset-permissions`: Comma-separated toolset list (permission-based servers with header-based permissions)
**AI agents working in this codebase must keep Intent Nodes up to date.** When you:

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

- `config`: Base64-encoded JSON containing session-specific context (if server has `sessionContext` enabled). Used for multi-tenant scenarios where each client needs different context values (API tokens, user IDs) passed to module loaders.
Intent Nodes should remain concise (~100 lines max). Focus on what an agent needs to work safely in that area — hidden knowledge that cannot be derived by reading the source code.

---
*This is the Intent Layer root. See leaf nodes for module-specific detail.*
112 changes: 9 additions & 103 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

Toolception is a dynamic MCP (Model Context Protocol) server toolkit for runtime toolset management. It allows grouping tools into toolsets and exposing only what's needed, when it's needed—reducing prompt/tool surface area for LLMs.

## Architecture & Intent Layer

**Start with `AGENTS.md`** — the intent layer root. It provides the architectural overview, critical invariants, module index, and links to module-specific Intent Nodes.

Read the relevant Intent Node before working in any area of the codebase.

## Common Commands

```bash
Expand Down Expand Up @@ -35,106 +41,6 @@ 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/AGENTS.md` - Type definitions and contracts
- `src/core/AGENTS.md` - ServerOrchestrator, DynamicToolManager, ToolRegistry
- `src/mode/AGENTS.md` - ModuleResolver, validation utilities
- `src/server/AGENTS.md` - createMcpServer, createPermissionBasedMcpServer
- `src/http/AGENTS.md` - FastifyTransport, endpoints, SSE
- `src/session/AGENTS.md` - SessionContextResolver, ClientResourceCache
- `src/permissions/AGENTS.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 `AGENTS.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

**ServerOrchestrator** (`src/core/ServerOrchestrator.ts`)
- Entry point that wires together all components
- Resolves startup mode (DYNAMIC vs STATIC) from configuration
- Creates ModuleResolver, DynamicToolManager, and ToolRegistry
- Registers meta-tools based on mode

**DynamicToolManager** (`src/core/DynamicToolManager.ts`)
- Manages toolset lifecycle (enable/disable)
- Enforces exposure policies (allowlist, denylist, maxActiveToolsets)
- Registers tools with the MCP server
- Tracks active toolsets and sends change notifications

**ToolRegistry** (`src/core/ToolRegistry.ts`)
- Central registry preventing tool name collisions
- Handles namespacing (e.g., `toolset.toolname`)
- Maps toolsets to their registered tools

**ModuleResolver** (`src/mode/ModuleResolver.ts`)
- Resolves tools from toolset definitions
- Loads module-produced tools via moduleLoaders
- Validates toolset names against catalog

### Server Creation APIs

Two main factory functions in `src/server/`:
- `createMcpServer` - Standard server with DYNAMIC or STATIC modes
- `createPermissionBasedMcpServer` - Per-client toolset access control

### HTTP Transport

**FastifyTransport** (`src/http/FastifyTransport.ts`)
- Fastify-based HTTP transport for MCP protocol
- Handles SSE streams, JSON-RPC requests
- Per-client server instances in DYNAMIC mode

**PermissionAwareFastifyTransport** (`src/http/PermissionAwareFastifyTransport.ts`)
- Extends FastifyTransport with permission checking
- Supports header-based or config-based permissions

### Session Context

**SessionContextResolver** (`src/session/SessionContextResolver.ts`)
- Parses query parameter (base64/json encoding)
- Filters allowed keys (whitelist enforcement)
- Merges session context with base context (shallow or deep)
- Generates cache key suffix for session differentiation

### Key Types (`src/types/index.ts`)

- `McpToolDefinition` - Tool with name, description, inputSchema, handler, optional annotations
- `ToolSetDefinition` - Groups tools with name, description, optional modules
- `ToolSetCatalog` - Record of toolset key to definition
- `ExposurePolicy` - Controls maxActiveToolsets, allowlist, denylist, namespacing
- `PermissionConfig` - Header or config-based permission source
- `SessionContextConfig` - Per-session context configuration (query params, encoding, merge strategy)
- `SessionRequestContext` - Request context (clientId, headers, query) for context resolvers

### Meta-tools (DYNAMIC mode)

Registered in `src/meta/registerMetaTools.ts`:
- `enable_toolset` / `disable_toolset` - Activate/deactivate toolsets
- `list_toolsets` / `describe_toolset` - Discovery
- `list_tools` - List currently registered tools

## Testing Patterns

Tests use Vitest with in-memory mocks. Key patterns:
Expand All @@ -145,10 +51,10 @@ Tests use Vitest with in-memory mocks. Key patterns:

### Key Test Files

- `tests/sessionContextResolver.test.ts` - Unit tests for SessionContextResolver (parsing, filtering, merging)
- `tests/sessionContextResolver.test.ts` - Unit tests for SessionContextResolver
- `tests/validateSessionContextConfig.test.ts` - Validation tests for SessionContextConfig
- `tests/sessionContext.integration.test.ts` - Integration tests for session context with HTTP transport
- `tests/e2e/dynamicMode.e2e.test.ts` - E2E tests for DYNAMIC mode and session context
- `tests/sessionContext.integration.test.ts` - Integration tests for session context
- `tests/e2e/dynamicMode.e2e.test.ts` - E2E tests for DYNAMIC mode
- `tests/e2e/staticMode.e2e.test.ts` - E2E tests for STATIC mode
- `tests/e2e/permissionBased.e2e.test.ts` - E2E tests for permission-based servers

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,7 @@ await client.close();

- **What**: Clients identify themselves via the `mcp-client-id` HTTP header on every request.
- **Who generates it**: The client. Use a stable identifier (e.g., UUID persisted locally).
- **If omitted**: The server assigns a one-off `anon-<uuid>` and skips caching; this is unsuitable for multi-request flows and SSE.
- **If omitted**: MCP protocol endpoints (`POST /mcp`, `GET /mcp`, `DELETE /mcp`) return a 400 error. Custom endpoints still accept anonymous clients with auto-generated IDs.

Examples (official MCP client)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "toolception",
"version": "0.6.1",
"version": "0.6.2",
"private": false,
"type": "module",
"main": "dist/index.js",
Expand Down
85 changes: 21 additions & 64 deletions src/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,88 +2,45 @@

## Purpose

Central orchestration layer that wires together all components. Manages toolset lifecycle, tool registration, and server initialization.
Central orchestration layer. 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
- **ServerOrchestrator** — Entry point wiring ModuleResolver + DynamicToolManager + ToolRegistry. Resolves startup mode, initializes toolsets, optionally registers meta-tools.
- **DynamicToolManager** — Enable/disable toolsets at runtime. Validates names, checks exposure policy, resolves tools via ModuleResolver, registers via ToolRegistry.
- **ToolRegistry** — Tool name registry with collision detection and optional namespacing (`toolset.toolName`).

## 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):

Meta-tools are registered with `ToolRegistry` under the reserved `_meta` toolset key (`META_TOOLSET_KEY` constant). This ensures collision detection with user-defined tools and makes meta-tools visible in `toolRegistry.list()` and `toolRegistry.listByToolset()`.
1. **All tools go through ToolRegistry** — Collision detection happens here only. Bypassing it causes silent name conflicts.
2. **Disable is state-only** — MCP SDK has no `server.removeTool()`; disabled toolsets' tools remain callable by clients. `activeToolsets` tracks logical state only.
3. **Notifications may fail silently** — "Not connected" errors are expected during SSE disconnect and are swallowed.
4. **Namespacing applied BEFORE collision check** — `mapAndValidate()` generates safe names first, then checks for conflicts.
5. **Toolset added to activeToolsets AFTER registration** — All tools must register successfully before toolset is marked active. Partial failures leave the toolset inactive but orphaned tools remain registered (MCP limitation).
6. **`enableToolsets()` batches notifications** — Calls `enableToolset(name, skipNotification=true)` per toolset, sends one `tools/list_changed` notification at the end.

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

**Both modes:**
- `list_tools` - List registered tool names (includes meta-tools)
```
enableToolset(name)
→ validateToolsetForEnable(name) → fail fast if invalid/already active
→ checkExposurePolicy(name) → fail fast if denied by allowlist/denylist/maxActive
→ resolveAndRegisterTools(name) → ModuleResolver + ToolRegistry
→ activeToolsets.add(name) — only after successful registration
→ notifyToolsChanged() — unless skipNotification
```

## 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)
- Using `_meta` as a toolset key in the catalog (reserved for meta-tools, rejected at startup)

## 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)
```
- Using `_meta` as a toolset key in the catalog (reserved for meta-tools — see `src/meta/AGENTS.md`)

## Dependencies

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

## See Also

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

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