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
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ For `enable_toolset`/`disable_toolset`, read `message` to adapt decisions (e.g.,

- `GET /healthz` - Health check
- `GET /tools` - List available toolsets and tools
- `POST /mcp` - MCP JSON-RPC requests
- `GET /mcp` - Server-sent events stream
- `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

Expand All @@ -65,3 +65,7 @@ Check `GET /tools` or server documentation to discover available custom endpoint
- `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)

### Query parameters

- `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.
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,23 @@ Two main factory functions in `src/server/`:
- 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)

Expand All @@ -98,8 +108,18 @@ Registered in `src/meta/registerMetaTools.ts`:
Tests use Vitest with in-memory mocks. Key patterns:
- Fake MCP server in `tests/helpers/fakes.ts`
- Unit tests alongside integration tests in `tests/`
- E2E tests in `tests/e2e/` for full server/client flows
- Smoke E2E tests in `tests/smoke-e2e/` for manual server/client testing

### Key Test Files

- `tests/sessionContextResolver.test.ts` - Unit tests for SessionContextResolver (parsing, filtering, merging)
- `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/e2e/staticMode.e2e.test.ts` - E2E tests for STATIC mode
- `tests/e2e/permissionBased.e2e.test.ts` - E2E tests for permission-based servers

## Build System

- Vite for bundling (`vite.config.ts`)
Expand Down
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Permission-based starter guide](#permission-based-starter-guide)
- [Permission configuration approaches](#permission-configuration-approaches)
- [Custom HTTP endpoints](#custom-http-endpoints)
- [Per-session context](#per-session-context)
- [API](#api)
- [createMcpServer](#createmcpserveroptions)
- [createPermissionBasedMcpServer](#createpermissionbasedmcpserveroptions)
Expand Down Expand Up @@ -39,6 +40,7 @@ Toolception addresses this by grouping tools into toolsets and letting you expos
- **Large or multi-domain catalogs**: You have >20–50 tools or multiple domains (e.g., search, data, billing) and don’t want to expose them all at once.
- **Task-specific workflows**: You want the client/agent to enable only the tools relevant to the current task.
- **Multi-tenant or policy needs**: Different users/tenants require different tool access or limits.
- **Per-session context**: You need different context values (API tokens, user IDs) for each client session passed to module loaders.
- **Permission-based access control**: You need to enforce client-specific toolset permissions for security, compliance, or multi-tenant isolation. Each client should only see and access the toolsets they're authorized to use, with server-side or header-based permission enforcement.
- **Collision-safe naming**: You need predictable, namespaced tool names to avoid conflicts.
- **Lazy loading**: Some tools are heavy and should be loaded on demand.
Expand Down Expand Up @@ -719,6 +721,88 @@ Custom endpoints cannot override built-in MCP paths:

See `examples/custom-endpoints-demo.ts` for a full working example with GET, POST, PUT, DELETE endpoints, pagination, and permission-aware handlers.

## Per-session context

Use the `sessionContext` option to enable per-client context values extracted from query parameters. This is useful for multi-tenant scenarios where each client needs different configuration (API tokens, user IDs, etc.) passed to module loaders.

### Basic usage

```ts
import { createMcpServer } from "toolception";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const { start } = await createMcpServer({
catalog: { /* ... */ },
moduleLoaders: { /* ... */ },
context: { baseValue: 'shared' }, // Base context for all sessions
sessionContext: {
enabled: true,
queryParam: {
name: 'config',
encoding: 'base64',
allowedKeys: ['API_TOKEN', 'USER_ID'], // Security: always specify
},
merge: 'shallow',
},
createServer: () => new McpServer({
name: "my-server",
version: "1.0.0",
capabilities: { tools: { listChanged: true } },
}),
http: { port: 3000 },
});

await start();
```

### Client connection

```bash
# Encode session config as base64
CONFIG=$(echo -n '{"API_TOKEN":"user-secret-token","USER_ID":"123"}' | base64)

# Connect with session config
curl -X POST "http://localhost:3000/mcp?config=$CONFIG" \
-H "mcp-client-id: my-client" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize",...}'
```

### Module loader receives merged context

```ts
const moduleLoaders = {
tenant: async (ctx: any) => {
// ctx = { baseValue: 'shared', API_TOKEN: 'user-secret-token', USER_ID: '123' }
return [/* tools using ctx.API_TOKEN */];
},
};
```

### Custom context resolver

For advanced use cases, provide a custom resolver function:

```ts
sessionContext: {
enabled: true,
queryParam: { allowedKeys: ['tenant_id'] },
contextResolver: (request, baseContext, parsedConfig) => ({
...baseContext,
...parsedConfig,
clientId: request.clientId,
timestamp: Date.now(),
}),
}
```

### Security considerations

- **Always specify `allowedKeys`**: Without a whitelist, any key in the query config is accepted
- **Fail-secure**: Invalid encoding silently falls back to base context
- **No logging of values**: Session config values are never logged
- **Filtered silently**: Disallowed keys are filtered without error messages

## API

### createMcpServer(options)
Expand Down Expand Up @@ -864,6 +948,28 @@ const moduleLoaders = {
};
```

#### options.sessionContext (optional)

`SessionContextConfig`

Configuration for per-session context extraction from query parameters. Enables multi-tenant use cases where each client session can have its own context values passed to module loaders. See [Per-session context](#per-session-context) for detailed usage examples.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | `boolean` | `true` | Whether session context extraction is enabled |
| `queryParam.name` | `string` | `'config'` | Query parameter name |
| `queryParam.encoding` | `'base64' \| 'json'` | `'base64'` | Encoding format |
| `queryParam.allowedKeys` | `string[]` | - | Whitelist of allowed keys (recommended for security) |
| `contextResolver` | `function` | - | Custom context resolver function |
| `merge` | `'shallow' \| 'deep'` | `'shallow'` | How to merge with base context |

Notes

- Session context is extracted per-request and merged with the base `context` option
- Each unique session config generates a different cache key, enabling per-tenant module caching
- Invalid encoding or parsing errors silently fall back to base context (fail-secure)
- Only applies to DYNAMIC mode servers; STATIC mode uses a single shared server instance

#### options.http (optional)

`{ host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean; customEndpoints?: CustomEndpointDefinition[] }`
Expand Down Expand Up @@ -970,6 +1076,14 @@ Same as `createMcpServer` - see [options.configSchema](#optionsconfigschema-opti

Same as `createMcpServer` - see [options.context](#optionscontext-optional).

#### options.sessionContext (optional)

`SessionContextConfig`

Session context is available in permission-based servers but has limited support. Because permission-based servers determine toolsets at connection time based on permissions, the session context cannot affect which toolsets are loaded. However, the merged context is still passed to module loaders.

**Note:** A warning is issued if `sessionContext` is used with `createPermissionBasedMcpServer`. For full session context support with per-session toolset caching, use `createMcpServer` with DYNAMIC mode.

### Meta-tools

Meta-tools are registered based on mode:
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.5.5",
"version": "0.6.0",
"private": false,
"type": "module",
"main": "dist/index.js",
Expand Down
Loading