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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ These are cross-cutting; see leaf Intent Nodes for module-specific invariants.
3. **Fail-secure** — Invalid inputs return empty objects/arrays, not errors
4. **Module loader errors are silent, registration failures are not** — A throwing loader is logged and the toolset activates with fewer tools. A failure while registering a startup toolset's tools (for example a duplicate tool name) makes `isReady()` return `false`: STATIC `createMcpServer` throws at startup and its transport answers the client's `initialize` with a 500 / `-32603` and drops the bundle. `createPermissionBasedMcpServer` is the exception: it enables toolsets in its bundle factory and continues on partial failure
5. **Disable unregisters tools** — `disableToolset` removes the toolset's tools from the MCP server via the SDK's `RegisteredTool.remove()`; see `src/core/AGENTS.md`
6. **Clients never share toolset state** — Every `mcp-client-id` (plus session-context cache key) gets its own `McpServer`, orchestrator and `NodeStreamableHTTPServerTransport`. Enabling or disabling a toolset, `tools/list_changed` notifications and session context (tokens) of one client are never visible to another. `tests/e2e/multiClient.e2e.test.ts` is the guard; keep it green on every SDK or transport change
7. **MCP SDK v2 and Zod 4 only** — Built on `@modelcontextprotocol/server` and `@modelcontextprotocol/node`. SDK v2 cannot read Zod 3 schemas, so tool and endpoint schemas must be Zod 4. There must be one installed copy of `zod` and of `@modelcontextprotocol/server` (`tests/dependencies.test.ts`)

## Module Index

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to `toolception` are listed here. Versions follow [semver](h

Releases before 0.7.0 are described in [GitHub Releases](https://github.com/code-rabi/toolception/releases).

## [Unreleased]

### Breaking

- Toolception now runs on MCP TypeScript SDK v2 and Zod 4. The `@modelcontextprotocol/sdk` dependency is replaced by `@modelcontextprotocol/server` and `@modelcontextprotocol/node`, and `zod` moves from ^3.25 to ^4.6.7. SDK v2 does not read Zod 3 schemas, so the two cannot be upgraded separately.
- **Migration:** change your imports to `@modelcontextprotocol/server` (`McpServer`) and `@modelcontextprotocol/client` (`Client`, `StreamableHTTPClientTransport`); install `zod` ^4.6.7; and write tool `inputSchema` values and `defineEndpoint` schemas with Zod 4. `z.record()` needs two arguments in Zod 4. Pass server capabilities as the second `McpServer` argument (`new McpServer(info, { capabilities })`); `capabilities` inside the first argument is rejected by the types.
- A tool whose `inputSchema` is a Zod 3 schema (or a raw shape holding one) now fails to enable with an `E_VALIDATION` error that names the tool.
- `createServer` factories must return a v2 `McpServer`; the removed `server.tool()`, `server.prompt()` and `server.resource()` are replaced by `registerTool`, `registerPrompt` and `registerResource`.
- `defineEndpoint` / `definePermissionAwareEndpoint` validation failures (`VALIDATION_ERROR`, HTTP 400) now carry Zod 4 issue objects in `error.details`: issue codes and messages differ from Zod 3 (for example `invalid_type` messages). Clients that match on the old text must be updated.
- Calling a tool the server no longer lists (for example a disabled toolset's tool) is now answered with a JSON-RPC error ("Tool ... not found") instead of a result with `isError: true`. This is SDK v2 behaviour.
- `FastifyTransport` and `PermissionAwareFastifyTransport` use `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`. Session handling, the per-client `mcp-client-id` model and the HTTP endpoints are unchanged: each client still gets its own server, transport and enabled/disabled toolset state.

### Fixed

- Dependency advisories: Zod 4.6.7 clears the high-severity `zod` finding (GHSA-mrhx-c7xc-m3w5); `body-parser`, `qs` and `ip-address` leave the tree with the v1 SDK's Express dependencies; `fastify` 5.12.8 and `fast-uri` 3.1.9 come from a lockfile refresh. Only the dev-only `minimatch` chain through `vite-plugin-dts` 4 remains, and it does not ship in the package.

## [0.8.0]

### Breaking
Expand Down
81 changes: 34 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,11 @@ Toolception addresses this by grouping tools into toolsets and letting you expos
### Step 1: Install

```bash
npm i toolception
npm i toolception @modelcontextprotocol/server zod
```

Toolception 0.9 is built on MCP TypeScript SDK v2 (`@modelcontextprotocol/server`) and needs Zod 4 (`zod` ^4.6.7); Zod 3 schemas are rejected. Install the same versions toolception depends on, so your app and toolception share one copy of each.

### Step 2: Import Toolception

```ts
Expand All @@ -98,7 +100,7 @@ const catalog = {

### Step 4: Define a tool

`inputSchema` should be a Zod raw shape (or a Zod object schema), the same form `McpServer.registerTool` takes. A plain JSON Schema object still works, but clients see an open object schema and arguments reach the handler unvalidated.
`inputSchema` should be a Zod 4 raw shape (or a Zod 4 object schema); toolception wraps a raw shape with `z.object()` before it calls `McpServer.registerTool`. A Zod 3 schema fails to enable its toolset with an `E_VALIDATION` error. A plain JSON Schema object still works, but clients see an open object schema and arguments reach the handler unvalidated.

```ts
import { z } from "zod";
Expand Down Expand Up @@ -140,15 +142,14 @@ const configSchema = {
### Step 7: Create the MCP SDK server and start Toolception

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

// You own the SDK server; pass a factory into Toolception (required in DYNAMIC mode)
const createServer = () =>
new McpServer({
name: "my-mcp-server",
version: "0.0.0",
capabilities: { tools: { listChanged: true } },
});
}, { capabilities: { tools: { listChanged: true } } });

const { start, close } = await createMcpServer({
catalog,
Expand Down Expand Up @@ -179,7 +180,7 @@ process.on("SIGTERM", async () => {
Enable some or ALL toolsets at bootstrap. In STATIC mode, a single server instance is created and reused for all clients:

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

const staticCatalog = {
search: { name: "Search", description: "Search tools", modules: ["search"] },
Expand All @@ -195,8 +196,7 @@ createMcpServer({
new McpServer({
name: "static-1",
version: "0.0.0",
capabilities: { tools: { listChanged: false } },
}),
}, { capabilities: { tools: { listChanged: false } } }),
});

// Load ALL toolsets
Expand All @@ -208,8 +208,7 @@ createMcpServer({
new McpServer({
name: "static-2",
version: "0.0.0",
capabilities: { tools: { listChanged: false } },
}),
}, { capabilities: { tools: { listChanged: false } } }),
});
```

Expand All @@ -220,14 +219,16 @@ Use `createPermissionBasedMcpServer` when you need to enforce client-specific to
### Step 1: Install

```bash
npm i toolception
npm i toolception @modelcontextprotocol/server zod
```

Toolception 0.9 is built on MCP TypeScript SDK v2 (`@modelcontextprotocol/server`) and needs Zod 4 (`zod` ^4.6.7); Zod 3 schemas are rejected. Install the same versions toolception depends on, so your app and toolception share one copy of each.

### Step 2: Import Toolception

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

### Step 3: Define a toolset catalog
Expand Down Expand Up @@ -309,8 +310,7 @@ const createServer = () =>
new McpServer({
name: "permission-header-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
}, { capabilities: { tools: { listChanged: false } } });

const { start, close } = await createPermissionBasedMcpServer({
catalog,
Expand All @@ -333,8 +333,7 @@ const createServer = () =>
new McpServer({
name: "permission-config-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
}, { capabilities: { tools: { listChanged: false } } });

const { start, close } = await createPermissionBasedMcpServer({
catalog,
Expand All @@ -361,8 +360,7 @@ const createServer = () =>
new McpServer({
name: "permission-resolver-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
}, { capabilities: { tools: { listChanged: false } } });

const { start, close } = await createPermissionBasedMcpServer({
catalog,
Expand Down Expand Up @@ -410,14 +408,13 @@ Use header-based permissions when you have an authentication gateway or proxy th

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

const createServer = () =>
new McpServer({
name: "permission-header-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
}, { capabilities: { tools: { listChanged: false } } });

const { start, close } = await createPermissionBasedMcpServer({
catalog: {
Expand Down Expand Up @@ -464,14 +461,13 @@ Use a static map when you have a fixed set of clients with known permissions. Th

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

const createServer = () =>
new McpServer({
name: "permission-config-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
}, { capabilities: { tools: { listChanged: false } } });

const { start, close } = await createPermissionBasedMcpServer({
catalog: {
Expand Down Expand Up @@ -522,14 +518,13 @@ Use a resolver function when you need custom logic to determine permissions, suc

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

const createServer = () =>
new McpServer({
name: "permission-resolver-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
});
}, { capabilities: { tools: { listChanged: false } } });

const { start, close } = await createPermissionBasedMcpServer({
catalog: {
Expand Down Expand Up @@ -723,7 +718,7 @@ Use the `sessionContext` option to enable per-client context values extracted fr

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

const { start } = await createMcpServer({
catalog: { /* ... */ },
Expand All @@ -741,8 +736,7 @@ const { start } = await createMcpServer({
createServer: () => new McpServer({
name: "my-server",
version: "1.0.0",
capabilities: { tools: { listChanged: true } },
}),
}, { capabilities: { tools: { listChanged: true } } }),
http: { port: 3000 },
});

Expand Down Expand Up @@ -1105,8 +1099,7 @@ Note: In STATIC mode, `enable_toolset`, `disable_toolset`, `list_toolsets`, and
When connecting to a permission-based server with header-based permissions, include the `mcp-toolset-permissions` header with a comma-separated list of toolsets:

```ts
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

const clientId = "my-client-id";
const allowedToolsets = ["user", "reports"]; // determined by your auth system
Expand Down Expand Up @@ -1140,8 +1133,7 @@ await client.close();
When connecting to a permission-based server with config-based permissions, only provide the `mcp-client-id` header. The server looks up permissions internally:

```ts
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

const clientId = "admin-client-id"; // matches server's staticMap or resolver

Expand Down Expand Up @@ -1178,8 +1170,7 @@ await client.close();
Examples (official MCP client)

```ts
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

// Create a stable client id (persist it for reuse across runs)
const clientId = "my-stable-client-id"; // e.g., from disk/env
Expand Down Expand Up @@ -1215,8 +1206,7 @@ await client.close();
Examples (official MCP client)

```ts
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

const clientId = "my-stable-client-id";
const transport = new StreamableHTTPClientTransport(
Expand Down Expand Up @@ -1382,7 +1372,7 @@ Create a server where each tenant has access to their own toolsets plus shared t

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

const { start, close } = await createPermissionBasedMcpServer({
catalog: {
Expand Down Expand Up @@ -1431,8 +1421,7 @@ const { start, close } = await createPermissionBasedMcpServer({
new McpServer({
name: "multi-tenant-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
}),
}, { capabilities: { tools: { listChanged: false } } }),
});

await start();
Expand All @@ -1444,7 +1433,7 @@ Integrate with an external authentication system by pre-loading permissions:

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

// Pre-load permissions from your auth system
// This should be done before server creation and cached
Expand Down Expand Up @@ -1486,8 +1475,7 @@ const { start, close } = await createPermissionBasedMcpServer({
new McpServer({
name: "auth-integrated-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
}),
}, { capabilities: { tools: { listChanged: false } } }),
});

await start();
Expand All @@ -1499,7 +1487,7 @@ Implement role-based access control with predefined role-to-toolset mappings:

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

// Define role-to-toolset mappings
const rolePermissions = {
Expand Down Expand Up @@ -1587,8 +1575,7 @@ const { start, close } = await createPermissionBasedMcpServer({
new McpServer({
name: "rbac-server",
version: "1.0.0",
capabilities: { tools: { listChanged: false } },
}),
}, { capabilities: { tools: { listChanged: false } } }),
});

await start();
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-endpoints-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* curl -X POST http://localhost:3000/api/users -H 'Content-Type: application/json' -d '{"name":"Alice","email":"alice@example.com"}'
*/

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
import {
createMcpServer,
Expand Down
Loading
Loading