toolception 0.2.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +945 -21
  2. package/dist/core/DynamicToolManager.d.ts +45 -2
  3. package/dist/core/DynamicToolManager.d.ts.map +1 -1
  4. package/dist/core/ServerOrchestrator.d.ts +24 -2
  5. package/dist/core/ServerOrchestrator.d.ts.map +1 -1
  6. package/dist/http/FastifyTransport.d.ts +11 -0
  7. package/dist/http/FastifyTransport.d.ts.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +940 -237
  11. package/dist/index.js.map +1 -1
  12. package/dist/meta/registerMetaTools.d.ts +14 -0
  13. package/dist/meta/registerMetaTools.d.ts.map +1 -1
  14. package/dist/mode/ModeResolver.d.ts +7 -0
  15. package/dist/mode/ModeResolver.d.ts.map +1 -1
  16. package/dist/permissions/PermissionAwareFastifyTransport.d.ts +47 -0
  17. package/dist/permissions/PermissionAwareFastifyTransport.d.ts.map +1 -0
  18. package/dist/permissions/PermissionResolver.d.ts +43 -0
  19. package/dist/permissions/PermissionResolver.d.ts.map +1 -0
  20. package/dist/permissions/createPermissionAwareBundle.d.ts +58 -0
  21. package/dist/permissions/createPermissionAwareBundle.d.ts.map +1 -0
  22. package/dist/permissions/validatePermissionConfig.d.ts +9 -0
  23. package/dist/permissions/validatePermissionConfig.d.ts.map +1 -0
  24. package/dist/server/createMcpServer.d.ts +4 -3
  25. package/dist/server/createMcpServer.d.ts.map +1 -1
  26. package/dist/server/createPermissionBasedMcpServer.d.ts +65 -0
  27. package/dist/server/createPermissionBasedMcpServer.d.ts.map +1 -0
  28. package/dist/session/ClientResourceCache.d.ts +34 -3
  29. package/dist/session/ClientResourceCache.d.ts.map +1 -1
  30. package/dist/types/index.d.ts +229 -0
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/package.json +7 -2
package/README.md CHANGED
@@ -8,7 +8,14 @@
8
8
  - [When and why to use Toolception](#when-and-why-to-use-toolception)
9
9
  - [Starter guide](#starter-guide)
10
10
  - [Static startup](#static-startup)
11
+ - [Permission-based starter guide](#permission-based-starter-guide)
12
+ - [Permission configuration approaches](#permission-configuration-approaches)
11
13
  - [API](#api)
14
+ - [createMcpServer](#createmcpserveroptions)
15
+ - [createPermissionBasedMcpServer](#createpermissionbasedmcpserveroptions)
16
+ - [Permission-based client integration](#permission-based-client-integration)
17
+ - [Permission-based security best practices](#permission-based-security-best-practices)
18
+ - [Permission-based common patterns](#permission-based-common-patterns)
12
19
  - [Client ID lifecycle](#client-id-lifecycle)
13
20
  - [Session ID lifecycle](#session-id-lifecycle)
14
21
  - [Tool types](#tool-types)
@@ -18,6 +25,7 @@
18
25
  ## When and why to use Toolception
19
26
 
20
27
  Building MCP servers with dozens or hundreds of tools often harms LLM performance and developer experience:
28
+
21
29
  - **Too many tools overwhelm selection**: Larger tool lists increase confusion and mis-selection rates.
22
30
  - **Token and schema bloat**: Long tool catalogs inflate prompts and latency.
23
31
  - **Name collisions and ambiguity**: Similar tool names across domains cause failures and fragile integrations.
@@ -26,13 +34,16 @@ Building MCP servers with dozens or hundreds of tools often harms LLM performanc
26
34
  Toolception addresses this by grouping tools into toolsets and letting you expose only what’s needed, when it’s needed.
27
35
 
28
36
  ### When to use Toolception
37
+
29
38
  - **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.
30
39
  - **Task-specific workflows**: You want the client/agent to enable only the tools relevant to the current task.
31
40
  - **Multi-tenant or policy needs**: Different users/tenants require different tool access or limits.
41
+ - **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.
32
42
  - **Collision-safe naming**: You need predictable, namespaced tool names to avoid conflicts.
33
43
  - **Lazy loading**: Some tools are heavy and should be loaded on demand.
34
44
 
35
45
  ### Why Toolception helps
46
+
36
47
  - **Toolsets**: Group related tools and expose minimal, coherent subsets per task.
37
48
  - **Dynamic mode (runtime control)**:
38
49
  - Enable toolsets on demand via meta-tools (`enable_toolset`, `disable_toolset`, `list_toolsets`, `describe_toolset`, `list_tools`).
@@ -51,10 +62,12 @@ Toolception addresses this by grouping tools into toolsets and letting you expos
51
62
  - `ModuleLoaders` are deterministic/idempotent for repeatable runs and caching.
52
63
 
53
64
  ### Choosing a mode
65
+
54
66
  - **Prefer DYNAMIC** when tool needs vary by task, you want tighter prompts, or you need runtime gating and lazy loading.
55
67
  - **Choose STATIC** when your tool needs are stable and small, or when your client cannot (or should not) perform runtime enable/disable operations.
56
68
 
57
69
  ### Typical flows
70
+
58
71
  - **Discovery-first (dynamic)**: Client calls `list_toolsets` → enables a set → calls namespaced tools (e.g., `core.ping`).
59
72
  - **Fixed pipeline (static)**: Server preloads named toolsets (or ALL) at startup; clients call `list_tools` and invoke as usual.
60
73
 
@@ -158,7 +171,7 @@ process.on("SIGTERM", async () => {
158
171
 
159
172
  ## Static startup
160
173
 
161
- Enable some or ALL toolsets at bootstrap. Note: provide a server or factory:
174
+ Enable some or ALL toolsets at bootstrap. In STATIC mode, a single server instance is created and reused for all clients:
162
175
 
163
176
  ```ts
164
177
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -168,29 +181,414 @@ const staticCatalog = {
168
181
  quotes: { name: "Quotes", description: "Market quotes", modules: ["quotes"] },
169
182
  };
170
183
 
184
+ // Load specific toolsets
171
185
  createMcpServer({
172
186
  catalog: staticCatalog,
173
187
  startup: { mode: "STATIC", toolsets: ["search", "quotes"] },
174
188
  http: { port: 3001 },
175
- server: new McpServer({
176
- name: "static-1",
177
- version: "0.0.0",
178
- capabilities: { tools: { listChanged: false } },
179
- }),
189
+ createServer: () =>
190
+ new McpServer({
191
+ name: "static-1",
192
+ version: "0.0.0",
193
+ capabilities: { tools: { listChanged: false } },
194
+ }),
180
195
  });
181
196
 
197
+ // Load ALL toolsets
182
198
  createMcpServer({
183
199
  catalog: staticCatalog,
184
200
  startup: { mode: "STATIC", toolsets: "ALL" },
185
201
  http: { port: 3002 },
186
- server: new McpServer({
187
- name: "static-2",
188
- version: "0.0.0",
189
- capabilities: { tools: { listChanged: false } },
202
+ createServer: () =>
203
+ new McpServer({
204
+ name: "static-2",
205
+ version: "0.0.0",
206
+ capabilities: { tools: { listChanged: false } },
207
+ }),
208
+ });
209
+ ```
210
+
211
+ ## Permission-based starter guide
212
+
213
+ Use `createPermissionBasedMcpServer` when you need to enforce client-specific toolset permissions. This is ideal for multi-tenant applications, security-sensitive environments, or when different clients should have different levels of access.
214
+
215
+ ### Step 1: Install
216
+
217
+ ```bash
218
+ npm i toolception
219
+ ```
220
+
221
+ ### Step 2: Import Toolception
222
+
223
+ ```ts
224
+ import { createPermissionBasedMcpServer } from "toolception";
225
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
226
+ ```
227
+
228
+ ### Step 3: Define a toolset catalog
229
+
230
+ ```ts
231
+ const catalog = {
232
+ admin: {
233
+ name: "Admin Tools",
234
+ description: "Administrative operations",
235
+ modules: ["admin"],
236
+ },
237
+ user: {
238
+ name: "User Tools",
239
+ description: "Standard user operations",
240
+ modules: ["user"],
241
+ },
242
+ };
243
+ ```
244
+
245
+ ### Step 4: Define tools
246
+
247
+ ```ts
248
+ const adminTool = {
249
+ name: "delete_user",
250
+ description: "Delete a user account",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ userId: { type: "string", description: "User ID to delete" },
255
+ },
256
+ required: ["userId"],
257
+ },
258
+ handler: async ({ userId }: { userId: string }) => ({
259
+ content: [{ type: "text", text: `User ${userId} deleted` }],
260
+ }),
261
+ } as const;
262
+
263
+ const userTool = {
264
+ name: "get_profile",
265
+ description: "Get user profile information",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {
269
+ userId: { type: "string", description: "User ID" },
270
+ },
271
+ required: ["userId"],
272
+ },
273
+ handler: async ({ userId }: { userId: string }) => ({
274
+ content: [{ type: "text", text: `Profile for ${userId}: {...}` }],
190
275
  }),
276
+ } as const;
277
+ ```
278
+
279
+ ### Step 5: Provide module loaders
280
+
281
+ ```ts
282
+ const moduleLoaders = {
283
+ admin: async () => [adminTool],
284
+ user: async () => [userTool],
285
+ };
286
+ ```
287
+
288
+ ### Step 6: Choose permission approach
289
+
290
+ You have two options for managing permissions:
291
+
292
+ **Header-Based Permissions:**
293
+
294
+ - Use when you have an authentication gateway/proxy
295
+ - Permissions passed via HTTP headers
296
+ - Good for dynamic, frequently-changing permissions
297
+ - Requires external validation of headers
298
+
299
+ **Config-Based Permissions:**
300
+
301
+ - Use when you want server-side control
302
+ - Permissions defined in server configuration
303
+ - Better security (no client-provided permission data)
304
+ - Good for stable permission structures
305
+
306
+ ### Step 7: Create the permission-based MCP server
307
+
308
+ **Option A: Header-Based Permissions**
309
+
310
+ ```ts
311
+ const createServer = () =>
312
+ new McpServer({
313
+ name: "permission-header-server",
314
+ version: "1.0.0",
315
+ capabilities: { tools: { listChanged: false } },
316
+ });
317
+
318
+ const { start, close } = await createPermissionBasedMcpServer({
319
+ catalog,
320
+ moduleLoaders,
321
+ permissions: {
322
+ source: "headers",
323
+ headerName: "mcp-toolset-permissions", // optional, this is default
324
+ },
325
+ http: { port: 3000 },
326
+ createServer,
327
+ });
328
+
329
+ await start();
330
+ ```
331
+
332
+ **Option B: Config-Based Permissions (Static Map)**
333
+
334
+ ```ts
335
+ const createServer = () =>
336
+ new McpServer({
337
+ name: "permission-config-server",
338
+ version: "1.0.0",
339
+ capabilities: { tools: { listChanged: false } },
340
+ });
341
+
342
+ const { start, close } = await createPermissionBasedMcpServer({
343
+ catalog,
344
+ moduleLoaders,
345
+ permissions: {
346
+ source: "config",
347
+ staticMap: {
348
+ "admin-client-id": ["admin", "user"],
349
+ "user-client-id": ["user"],
350
+ },
351
+ defaultPermissions: [], // unknown clients get no toolsets
352
+ },
353
+ http: { port: 3000 },
354
+ createServer,
355
+ });
356
+
357
+ await start();
358
+ ```
359
+
360
+ **Option C: Config-Based Permissions (Resolver Function)**
361
+
362
+ ```ts
363
+ const createServer = () =>
364
+ new McpServer({
365
+ name: "permission-resolver-server",
366
+ version: "1.0.0",
367
+ capabilities: { tools: { listChanged: false } },
368
+ });
369
+
370
+ const { start, close } = await createPermissionBasedMcpServer({
371
+ catalog,
372
+ moduleLoaders,
373
+ permissions: {
374
+ source: "config",
375
+ resolver: (clientId: string) => {
376
+ // Your custom permission logic
377
+ if (clientId.startsWith("admin-")) {
378
+ return ["admin", "user"];
379
+ }
380
+ if (clientId.startsWith("user-")) {
381
+ return ["user"];
382
+ }
383
+ return [];
384
+ },
385
+ defaultPermissions: [],
386
+ },
387
+ http: { port: 3000 },
388
+ createServer,
389
+ });
390
+
391
+ await start();
392
+ ```
393
+
394
+ ### Step 8: Graceful shutdown
395
+
396
+ ```ts
397
+ process.on("SIGINT", async () => {
398
+ await close();
399
+ process.exit(0);
400
+ });
401
+
402
+ process.on("SIGTERM", async () => {
403
+ await close();
404
+ process.exit(0);
405
+ });
406
+ ```
407
+
408
+ ## Permission configuration approaches
409
+
410
+ ### Header-Based Permissions Setup
411
+
412
+ Use header-based permissions when you have an authentication gateway or proxy that validates and sets permission headers. This approach is flexible for dynamic permissions but requires external header validation.
413
+
414
+ ```ts
415
+ import { createPermissionBasedMcpServer } from "toolception";
416
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
417
+
418
+ const createServer = () =>
419
+ new McpServer({
420
+ name: "permission-header-server",
421
+ version: "1.0.0",
422
+ capabilities: { tools: { listChanged: false } },
423
+ });
424
+
425
+ const { start, close } = await createPermissionBasedMcpServer({
426
+ catalog: {
427
+ admin: {
428
+ name: "Admin",
429
+ description: "Admin tools",
430
+ modules: ["admin"],
431
+ },
432
+ user: {
433
+ name: "User",
434
+ description: "User tools",
435
+ modules: ["user"],
436
+ },
437
+ },
438
+ moduleLoaders: {
439
+ admin: async () => [
440
+ /* admin tools */
441
+ ],
442
+ user: async () => [
443
+ /* user tools */
444
+ ],
445
+ },
446
+ permissions: {
447
+ source: "headers",
448
+ headerName: "mcp-toolset-permissions", // optional, this is default
449
+ },
450
+ http: { port: 3000 },
451
+ createServer,
452
+ });
453
+
454
+ await start();
455
+ ```
456
+
457
+ **When to use:**
458
+
459
+ - You have an authentication gateway/proxy that validates requests
460
+ - Permissions change frequently or are computed per-request
461
+ - You can ensure headers are cryptographically signed or validated
462
+ - Your auth system is external to the MCP server
463
+
464
+ ### Config-Based Permissions Setup (Static Map)
465
+
466
+ Use a static map when you have a fixed set of clients with known permissions. This provides server-side control and better security.
467
+
468
+ ```ts
469
+ import { createPermissionBasedMcpServer } from "toolception";
470
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
471
+
472
+ const createServer = () =>
473
+ new McpServer({
474
+ name: "permission-config-server",
475
+ version: "1.0.0",
476
+ capabilities: { tools: { listChanged: false } },
477
+ });
478
+
479
+ const { start, close } = await createPermissionBasedMcpServer({
480
+ catalog: {
481
+ admin: {
482
+ name: "Admin",
483
+ description: "Admin tools",
484
+ modules: ["admin"],
485
+ },
486
+ user: {
487
+ name: "User",
488
+ description: "User tools",
489
+ modules: ["user"],
490
+ },
491
+ },
492
+ moduleLoaders: {
493
+ admin: async () => [
494
+ /* admin tools */
495
+ ],
496
+ user: async () => [
497
+ /* user tools */
498
+ ],
499
+ },
500
+ permissions: {
501
+ source: "config",
502
+ staticMap: {
503
+ "admin-client-id": ["admin", "user"],
504
+ "user-client-id": ["user"],
505
+ },
506
+ defaultPermissions: [], // clients not in map get no toolsets
507
+ },
508
+ http: { port: 3000 },
509
+ createServer,
510
+ });
511
+
512
+ await start();
513
+ ```
514
+
515
+ **When to use:**
516
+
517
+ - You have a fixed set of known clients
518
+ - Permissions are relatively stable
519
+ - You want the highest security level
520
+ - You want to avoid trusting client-provided data
521
+
522
+ ### Config-Based Permissions Setup (Resolver Function)
523
+
524
+ Use a resolver function when you need custom logic to determine permissions, such as looking up from a database or applying complex rules.
525
+
526
+ ```ts
527
+ import { createPermissionBasedMcpServer } from "toolception";
528
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
529
+
530
+ const createServer = () =>
531
+ new McpServer({
532
+ name: "permission-resolver-server",
533
+ version: "1.0.0",
534
+ capabilities: { tools: { listChanged: false } },
535
+ });
536
+
537
+ const { start, close } = await createPermissionBasedMcpServer({
538
+ catalog: {
539
+ admin: {
540
+ name: "Admin",
541
+ description: "Admin tools",
542
+ modules: ["admin"],
543
+ },
544
+ user: {
545
+ name: "User",
546
+ description: "User tools",
547
+ modules: ["user"],
548
+ },
549
+ },
550
+ moduleLoaders: {
551
+ admin: async () => [
552
+ /* admin tools */
553
+ ],
554
+ user: async () => [
555
+ /* user tools */
556
+ ],
557
+ },
558
+ permissions: {
559
+ source: "config",
560
+ resolver: (clientId: string) => {
561
+ // Custom logic - could check database, config file, etc.
562
+ if (clientId.startsWith("admin-")) {
563
+ return ["admin", "user"];
564
+ }
565
+ if (clientId.startsWith("user-")) {
566
+ return ["user"];
567
+ }
568
+ return [];
569
+ },
570
+ staticMap: {
571
+ // optional fallback
572
+ "special-client": ["admin"],
573
+ },
574
+ defaultPermissions: [],
575
+ },
576
+ http: { port: 3000 },
577
+ createServer,
191
578
  });
579
+
580
+ await start();
192
581
  ```
193
582
 
583
+ **When to use:**
584
+
585
+ - You need custom permission logic
586
+ - Permissions are computed based on client ID patterns or attributes
587
+ - You want to integrate with existing permission systems
588
+ - You need fallback behavior with staticMap
589
+
590
+ **Note:** Resolver functions must be synchronous. If you need to fetch permissions from external sources, do so before server creation and cache the results.
591
+
194
592
  ## API
195
593
 
196
594
  ### createMcpServer(options)
@@ -270,9 +668,11 @@ Startup precedence and validation
270
668
 
271
669
  #### options.registerMetaTools (optional)
272
670
 
273
- `boolean` (default: true in DYNAMIC mode; false in STATIC unless explicitly set)
671
+ `boolean` (default: true in DYNAMIC mode; false in STATIC mode)
274
672
 
275
- - Whether to register management tools like `enable_toolset`, `disable_toolset`, `list_tools`.
673
+ - Whether to register meta-tools for toolset management.
674
+ - In DYNAMIC mode: Registers all meta-tools (`enable_toolset`, `disable_toolset`, `list_toolsets`, `describe_toolset`, `list_tools`).
675
+ - In STATIC mode: Only registers `list_tools` (other meta-tools are not applicable since toolsets are fixed at startup).
276
676
 
277
677
  #### options.exposurePolicy (optional)
278
678
 
@@ -352,13 +752,181 @@ Required factory to create the SDK server instance(s).
352
752
 
353
753
  - JSON Schema exposed at `GET /.well-known/mcp-config` for client discovery.
354
754
 
755
+ ### createPermissionBasedMcpServer(options)
756
+
757
+ Creates a permission-aware MCP server where each client receives only the toolsets they're authorized to access. This function provides a separate API for permission-based scenarios while maintaining the same interface as `createMcpServer`.
758
+
759
+ Requirements
760
+
761
+ - `createServer` must be provided
762
+ - `permissions` configuration must be provided
763
+ - Server operates in STATIC mode per-client (toolsets determined by permissions)
764
+ - Each client gets an isolated server instance with their specific toolsets
765
+
766
+ #### options.permissions (required)
767
+
768
+ `PermissionConfig`
769
+
770
+ Defines how client permissions are resolved and enforced.
771
+
772
+ **Permission Source Types**
773
+
774
+ | Source | Description | Use Case | Security Level |
775
+ | --------- | ------------------------------------- | ---------------------------------- | ------------------------------------- |
776
+ | `headers` | Read permissions from request headers | Behind authenticated proxy/gateway | Medium (requires external validation) |
777
+ | `config` | Server-side permission lookup | Direct server control | High (server-controlled) |
778
+
779
+ **Header-Based Configuration**
780
+
781
+ | Field | Type | Default | Description |
782
+ | ------------ | ----------- | --------------------------- | --------------------------------------------------- |
783
+ | `source` | `'headers'` | required | Indicates header-based permissions |
784
+ | `headerName` | `string` | `'mcp-toolset-permissions'` | Header name containing comma-separated toolset list |
785
+
786
+ **Config-Based Configuration**
787
+
788
+ | Field | Type | Required | Description |
789
+ | -------------------- | -------------------------------- | ---------------------------- | -------------------------------------------------------- |
790
+ | `source` | `'config'` | yes | Indicates config-based permissions |
791
+ | `staticMap` | `Record<string, string[]>` | one of staticMap or resolver | Maps client IDs to toolset arrays |
792
+ | `resolver` | `(clientId: string) => string[]` | one of staticMap or resolver | Function returning toolset array for client |
793
+ | `defaultPermissions` | `string[]` | no | Fallback permissions for unknown clients (default: `[]`) |
794
+
795
+ **Notes**
796
+
797
+ - For config-based permissions, at least one of `staticMap` or `resolver` must be provided
798
+ - If both are provided, `resolver` is tried first, then `staticMap`, then `defaultPermissions`
799
+ - Resolver functions must be synchronous and return string arrays
800
+ - Invalid toolset names in permissions are filtered out during server creation
801
+
802
+ #### options.catalog (required)
803
+
804
+ Same as `createMcpServer` - see [options.catalog](#optionscatalog-required).
805
+
806
+ #### options.moduleLoaders (optional)
807
+
808
+ Same as `createMcpServer` - see [options.moduleLoaders](#optionsmoduleloaders-optional).
809
+
810
+ #### options.exposurePolicy (optional)
811
+
812
+ `ExposurePolicy` (partial support)
813
+
814
+ Permission-based servers only support `namespaceToolsWithSetKey`. Other policy fields are ignored because toolset access is controlled by permissions:
815
+
816
+ | Field | Support |
817
+ | -------------------------- | ------------------------------------------------- |
818
+ | `namespaceToolsWithSetKey` | ✅ Supported (default: true) |
819
+ | `allowlist` | ⚠️ Ignored (determined by client permissions) |
820
+ | `denylist` | ⚠️ Ignored (use permissions instead) |
821
+ | `maxActiveToolsets` | ⚠️ Ignored (determined by permission count) |
822
+ | `onLimitExceeded` | ⚠️ Ignored (no toolset limits enforced) |
823
+
824
+ **Note:** If you provide ignored options, the server will log warnings at startup to alert you.
825
+
826
+ #### options.http (optional)
827
+
828
+ Same as `createMcpServer` - see [options.http](#optionshttp-optional).
829
+
830
+ #### options.createServer (required)
831
+
832
+ Same as `createMcpServer` - see [options.createServer](#optionscreateserver-optional).
833
+
834
+ #### options.configSchema (optional)
835
+
836
+ Same as `createMcpServer` - see [options.configSchema](#optionsconfigschema-optional).
837
+
838
+ #### options.context (optional)
839
+
840
+ Same as `createMcpServer` - see [options.context](#optionscontext-optional).
841
+
355
842
  ### Meta-tools
356
843
 
357
- Enabled by default when mode is DYNAMIC (or when `registerMetaTools` is true):
844
+ Meta-tools are registered based on mode:
845
+
846
+ **DYNAMIC mode** (registered by default, or when `registerMetaTools` is true):
847
+
848
+ - `enable_toolset` - Enable a toolset by name
849
+ - `disable_toolset` - Disable a toolset by name (state only; tools remain registered)
850
+ - `list_toolsets` - List available toolsets with active status
851
+ - `describe_toolset` - Describe a specific toolset with definition and tools
852
+ - `list_tools` - List currently registered tool names
853
+
854
+ **STATIC mode** (when `registerMetaTools` is true):
358
855
 
359
- - `enable_toolset`, `disable_toolset`, `list_tools`
360
- Only in DYNAMIC mode:
361
- - `list_toolsets`, `describe_toolset`
856
+ - `list_tools` - List currently registered tool names
857
+
858
+ Note: In STATIC mode, `enable_toolset`, `disable_toolset`, `list_toolsets`, and `describe_toolset` are not available since toolsets are fixed at startup.
859
+
860
+ ## Permission-based client integration
861
+
862
+ ### Using Header-Based Permissions
863
+
864
+ When connecting to a permission-based server with header-based permissions, include the `mcp-toolset-permissions` header with a comma-separated list of toolsets:
865
+
866
+ ```ts
867
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
868
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
869
+
870
+ const clientId = "my-client-id";
871
+ const allowedToolsets = ["user", "reports"]; // determined by your auth system
872
+
873
+ const transport = new StreamableHTTPClientTransport(
874
+ new URL("http://localhost:3000/mcp"),
875
+ {
876
+ requestInit: {
877
+ headers: {
878
+ "mcp-client-id": clientId,
879
+ "mcp-toolset-permissions": allowedToolsets.join(","),
880
+ },
881
+ },
882
+ }
883
+ );
884
+
885
+ const client = new Client({ name: "example-client", version: "1.0.0" });
886
+ await client.connect(transport);
887
+
888
+ // Client can only access tools from 'user' and 'reports' toolsets
889
+ const tools = await client.listTools();
890
+ console.log(tools); // Only shows user.* and reports.* tools
891
+
892
+ await client.close();
893
+ ```
894
+
895
+ **Important:** Your application layer must validate and potentially sign/encrypt the permission header to prevent tampering. The MCP server trusts the header value as-is.
896
+
897
+ ### Using Config-Based Permissions
898
+
899
+ When connecting to a permission-based server with config-based permissions, only provide the `mcp-client-id` header. The server looks up permissions internally:
900
+
901
+ ```ts
902
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
903
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
904
+
905
+ const clientId = "admin-client-id"; // matches server's staticMap or resolver
906
+
907
+ const transport = new StreamableHTTPClientTransport(
908
+ new URL("http://localhost:3000/mcp"),
909
+ {
910
+ requestInit: {
911
+ headers: {
912
+ "mcp-client-id": clientId,
913
+ // No permission header needed - server looks up permissions
914
+ },
915
+ },
916
+ }
917
+ );
918
+
919
+ const client = new Client({ name: "example-client", version: "1.0.0" });
920
+ await client.connect(transport);
921
+
922
+ // Client receives toolsets based on server configuration
923
+ const tools = await client.listTools();
924
+ console.log(tools); // Shows tools based on server's permission config
925
+
926
+ await client.close();
927
+ ```
928
+
929
+ **Security:** Config-based permissions provide better security since the client cannot influence their own permissions. Ensure your client IDs are authenticated and validated before reaching the MCP server.
362
930
 
363
931
  ## Client ID lifecycle
364
932
 
@@ -431,6 +999,360 @@ console.log(ping);
431
999
  await client.close();
432
1000
  ```
433
1001
 
1002
+ ## Permission-based security best practices
1003
+
1004
+ ### When to Use Each Approach
1005
+
1006
+ **Use Header-Based Permissions When:**
1007
+
1008
+ - You have an authentication gateway/proxy that validates and sets headers
1009
+ - You need dynamic permissions that change frequently
1010
+ - Your auth system is external to the MCP server
1011
+ - You can ensure headers are cryptographically signed or validated
1012
+
1013
+ **Use Config-Based Permissions When:**
1014
+
1015
+ - You want server-side control over permissions
1016
+ - Permissions are relatively stable
1017
+ - You need the highest security level
1018
+ - You want to avoid trusting client-provided data
1019
+
1020
+ ### Authentication and Authorization Patterns
1021
+
1022
+ **Header-Based Pattern:**
1023
+
1024
+ ```
1025
+ Client → Auth Gateway → MCP Server
1026
+ (validates,
1027
+ sets headers)
1028
+ ```
1029
+
1030
+ The auth gateway must:
1031
+
1032
+ 1. Authenticate the client
1033
+ 2. Determine authorized toolsets
1034
+ 3. Set `mcp-toolset-permissions` header
1035
+ 4. Optionally sign/encrypt headers to prevent tampering
1036
+
1037
+ **Config-Based Pattern:**
1038
+
1039
+ ```
1040
+ Client → MCP Server → Permission Lookup
1041
+ (validates (staticMap or
1042
+ client-id) resolver)
1043
+ ```
1044
+
1045
+ The MCP server:
1046
+
1047
+ 1. Receives client-id
1048
+ 2. Looks up permissions internally
1049
+ 3. No trust in client-provided permission data
1050
+
1051
+ ### Header Validation and Signing
1052
+
1053
+ If using header-based permissions, implement validation to prevent tampering:
1054
+
1055
+ ```ts
1056
+ import crypto from "crypto";
1057
+
1058
+ // Example: Using HMAC to sign permission headers
1059
+ function signPermissions(
1060
+ clientId: string,
1061
+ toolsets: string[],
1062
+ secret: string
1063
+ ): string {
1064
+ const data = `${clientId}:${toolsets.join(",")}`;
1065
+ const signature = crypto
1066
+ .createHmac("sha256", secret)
1067
+ .update(data)
1068
+ .digest("hex");
1069
+ return `${toolsets.join(",")};sig=${signature}`;
1070
+ }
1071
+
1072
+ function verifyPermissions(
1073
+ clientId: string,
1074
+ headerValue: string,
1075
+ secret: string
1076
+ ): string[] {
1077
+ const [toolsetsStr, sigPart] = headerValue.split(";sig=");
1078
+ const expectedSig = crypto
1079
+ .createHmac("sha256", secret)
1080
+ .update(`${clientId}:${toolsetsStr}`)
1081
+ .digest("hex");
1082
+
1083
+ if (sigPart !== expectedSig) {
1084
+ throw new Error("Invalid permission signature");
1085
+ }
1086
+
1087
+ return toolsetsStr.split(",").map((s) => s.trim());
1088
+ }
1089
+
1090
+ // In your auth gateway:
1091
+ const clientId = "user-123";
1092
+ const allowedToolsets = ["user", "reports"];
1093
+ const signedHeader = signPermissions(clientId, allowedToolsets, SECRET_KEY);
1094
+
1095
+ // Forward to MCP server with signed header
1096
+ fetch("http://mcp-server:3000/mcp", {
1097
+ headers: {
1098
+ "mcp-client-id": clientId,
1099
+ "mcp-toolset-permissions": signedHeader,
1100
+ },
1101
+ });
1102
+ ```
1103
+
1104
+ ### Security Considerations
1105
+
1106
+ **Header-Based Permissions:**
1107
+
1108
+ - **Risk:** Client can potentially manipulate headers if not properly secured
1109
+ - **Mitigation:** Always validate/sign headers in your application layer
1110
+ - **Recommendation:** Use only behind authenticated reverse proxy or gateway
1111
+ - **Best Practice:** Implement header signing with HMAC or JWT
1112
+
1113
+ **Config-Based Permissions:**
1114
+
1115
+ - **Benefit:** Server-side permission storage provides stronger security
1116
+ - **Recommendation:** Preferred for production environments
1117
+ - **Best Practice:** Authenticate client IDs before they reach the MCP server
1118
+ - **Note:** No client-side permission data exposure
1119
+
1120
+ **General Security:**
1121
+
1122
+ - **Permission Caching:** Permissions are cached per client session. Invalidate sessions when permissions change.
1123
+ - **Client Isolation:** Each client gets an isolated server instance. No cross-client permission leakage.
1124
+ - **Error Messages:** The server avoids exposing unauthorized toolset names in error responses.
1125
+ - **Client ID Validation:** Always validate and authenticate client IDs in your application layer before requests reach the MCP server.
1126
+
1127
+ ### Error Handling and Information Disclosure
1128
+
1129
+ When a client attempts to access unauthorized toolsets:
1130
+
1131
+ - The server returns a generic "Access denied" error
1132
+ - Unauthorized toolset names are not exposed in error messages
1133
+ - This prevents information disclosure about available toolsets
1134
+ - Clients only see tools they're authorized to access via `listTools()`
1135
+
1136
+ ## Permission-based common patterns
1137
+
1138
+ ### Multi-Tenant Server Setup
1139
+
1140
+ Create a server where each tenant has access to their own toolsets plus shared tools:
1141
+
1142
+ ```ts
1143
+ import { createPermissionBasedMcpServer } from "toolception";
1144
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1145
+
1146
+ const { start, close } = await createPermissionBasedMcpServer({
1147
+ catalog: {
1148
+ "tenant-a-tools": {
1149
+ name: "Tenant A",
1150
+ description: "Tools for tenant A",
1151
+ modules: ["tenant-a"],
1152
+ },
1153
+ "tenant-b-tools": {
1154
+ name: "Tenant B",
1155
+ description: "Tools for tenant B",
1156
+ modules: ["tenant-b"],
1157
+ },
1158
+ "shared-tools": {
1159
+ name: "Shared",
1160
+ description: "Shared tools",
1161
+ modules: ["shared"],
1162
+ },
1163
+ },
1164
+ moduleLoaders: {
1165
+ "tenant-a": async () => [
1166
+ /* tenant A specific tools */
1167
+ ],
1168
+ "tenant-b": async () => [
1169
+ /* tenant B specific tools */
1170
+ ],
1171
+ shared: async () => [
1172
+ /* shared tools */
1173
+ ],
1174
+ },
1175
+ permissions: {
1176
+ source: "config",
1177
+ resolver: (clientId: string) => {
1178
+ const [tenant] = clientId.split("-");
1179
+ if (tenant === "tenantA") {
1180
+ return ["tenant-a-tools", "shared-tools"];
1181
+ }
1182
+ if (tenant === "tenantB") {
1183
+ return ["tenant-b-tools", "shared-tools"];
1184
+ }
1185
+ return ["shared-tools"]; // unknown tenants get only shared tools
1186
+ },
1187
+ },
1188
+ http: { port: 3000 },
1189
+ createServer: () =>
1190
+ new McpServer({
1191
+ name: "multi-tenant-server",
1192
+ version: "1.0.0",
1193
+ capabilities: { tools: { listChanged: false } },
1194
+ }),
1195
+ });
1196
+
1197
+ await start();
1198
+ ```
1199
+
1200
+ ### Integration with External Auth Systems
1201
+
1202
+ Integrate with an external authentication system by pre-loading permissions:
1203
+
1204
+ ```ts
1205
+ import { createPermissionBasedMcpServer } from "toolception";
1206
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1207
+
1208
+ // Pre-load permissions from your auth system
1209
+ // This should be done before server creation and cached
1210
+ const permissionCache = new Map<string, string[]>();
1211
+
1212
+ async function loadPermissionsFromAuthSystem() {
1213
+ // Fetch permissions from your auth system
1214
+ // This is just an example - implement according to your system
1215
+ const users = await authSystem.getAllUsers();
1216
+ for (const user of users) {
1217
+ const permissions = await authSystem.getUserPermissions(user.id);
1218
+ permissionCache.set(user.id, permissions.allowedToolsets);
1219
+ }
1220
+ }
1221
+
1222
+ // Load permissions at startup
1223
+ await loadPermissionsFromAuthSystem();
1224
+
1225
+ // Optionally refresh permissions periodically
1226
+ setInterval(loadPermissionsFromAuthSystem, 5 * 60 * 1000); // every 5 minutes
1227
+
1228
+ const { start, close } = await createPermissionBasedMcpServer({
1229
+ catalog: {
1230
+ /* your toolsets */
1231
+ },
1232
+ moduleLoaders: {
1233
+ /* your loaders */
1234
+ },
1235
+ permissions: {
1236
+ source: "config",
1237
+ resolver: (clientId: string) => {
1238
+ // Synchronous lookup from pre-loaded cache
1239
+ return permissionCache.get(clientId) || [];
1240
+ },
1241
+ defaultPermissions: ["public"], // unauthenticated users get public tools
1242
+ },
1243
+ http: { port: 3000 },
1244
+ createServer: () =>
1245
+ new McpServer({
1246
+ name: "auth-integrated-server",
1247
+ version: "1.0.0",
1248
+ capabilities: { tools: { listChanged: false } },
1249
+ }),
1250
+ });
1251
+
1252
+ await start();
1253
+ ```
1254
+
1255
+ ### Role-Based Access Control (RBAC)
1256
+
1257
+ Implement role-based access control with predefined role-to-toolset mappings:
1258
+
1259
+ ```ts
1260
+ import { createPermissionBasedMcpServer } from "toolception";
1261
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1262
+
1263
+ // Define role-to-toolset mappings
1264
+ const rolePermissions = {
1265
+ admin: ["admin-tools", "user-tools", "reports", "analytics"],
1266
+ manager: ["user-tools", "reports", "analytics"],
1267
+ user: ["user-tools", "reports"],
1268
+ guest: ["public-tools"],
1269
+ };
1270
+
1271
+ // Map client IDs to roles (could come from database, JWT claims, etc.)
1272
+ function getRoleForClient(clientId: string): string {
1273
+ // Example: extract role from client ID or look up in database
1274
+ if (clientId.startsWith("admin-")) return "admin";
1275
+ if (clientId.startsWith("manager-")) return "manager";
1276
+ if (clientId.startsWith("user-")) return "user";
1277
+ return "guest";
1278
+ }
1279
+
1280
+ const { start, close } = await createPermissionBasedMcpServer({
1281
+ catalog: {
1282
+ "admin-tools": {
1283
+ name: "Admin",
1284
+ description: "Admin tools",
1285
+ modules: ["admin"],
1286
+ },
1287
+ "user-tools": {
1288
+ name: "User",
1289
+ description: "User tools",
1290
+ modules: ["user"],
1291
+ },
1292
+ reports: {
1293
+ name: "Reports",
1294
+ description: "Reporting tools",
1295
+ modules: ["reports"],
1296
+ },
1297
+ analytics: {
1298
+ name: "Analytics",
1299
+ description: "Analytics tools",
1300
+ modules: ["analytics"],
1301
+ },
1302
+ "public-tools": {
1303
+ name: "Public",
1304
+ description: "Public tools",
1305
+ modules: ["public"],
1306
+ },
1307
+ },
1308
+ moduleLoaders: {
1309
+ admin: async () => [
1310
+ /* admin tools */
1311
+ ],
1312
+ user: async () => [
1313
+ /* user tools */
1314
+ ],
1315
+ reports: async () => [
1316
+ /* report tools */
1317
+ ],
1318
+ analytics: async () => [
1319
+ /* analytics tools */
1320
+ ],
1321
+ public: async () => [
1322
+ /* public tools */
1323
+ ],
1324
+ },
1325
+ permissions: {
1326
+ source: "config",
1327
+ staticMap: {
1328
+ // Known admin users
1329
+ "admin-user-1": rolePermissions.admin,
1330
+ "admin-user-2": rolePermissions.admin,
1331
+ // Known managers
1332
+ "manager-user-1": rolePermissions.manager,
1333
+ // Known regular users
1334
+ "regular-user-1": rolePermissions.user,
1335
+ "regular-user-2": rolePermissions.user,
1336
+ },
1337
+ resolver: (clientId: string) => {
1338
+ // Dynamic role lookup for clients not in static map
1339
+ const role = getRoleForClient(clientId);
1340
+ return rolePermissions[role] || rolePermissions.guest;
1341
+ },
1342
+ defaultPermissions: rolePermissions.guest,
1343
+ },
1344
+ http: { port: 3000 },
1345
+ createServer: () =>
1346
+ new McpServer({
1347
+ name: "rbac-server",
1348
+ version: "1.0.0",
1349
+ capabilities: { tools: { listChanged: false } },
1350
+ }),
1351
+ });
1352
+
1353
+ await start();
1354
+ ```
1355
+
434
1356
  ## Tool types
435
1357
 
436
1358
  - Direct tools: defined inline under `catalog[toolset].tools` and registered when that toolset is enabled.
@@ -442,17 +1364,19 @@ Note on dynamic mode: Both direct and module-produced tools are supported. Modul
442
1364
 
443
1365
  ## Startup modes
444
1366
 
445
- The server operates in one of two primary modes (legacy load-all is not recommended here):
1367
+ The server operates in one of two primary modes:
446
1368
 
447
- 1. Dynamic mode (startup.mode = "DYNAMIC")
1369
+ 1. **Dynamic mode** (`startup.mode = "DYNAMIC"`)
448
1370
 
449
- - Starts with meta-tools for runtime management: `enable_toolset`, `disable_toolset`, `list_toolsets`, `describe_toolset`, and `list_tools` (always available)
1371
+ - All meta-tools registered: `enable_toolset`, `disable_toolset`, `list_toolsets`, `describe_toolset`, `list_tools`
450
1372
  - Tools are loaded on-demand via meta-tool calls
1373
+ - Each client gets an isolated server instance
451
1374
  - Best for flexible, task-specific workflows where tool needs change
452
1375
 
453
- 2. Static mode (startup.mode = "STATIC")
1376
+ 2. **Static mode** (`startup.mode = "STATIC"`)
454
1377
  - Pre-loads specific toolsets at startup (`toolsets` array or "ALL")
455
- - Meta-tools limited to `list_tools` by default
1378
+ - Only `list_tools` meta-tool is available (toolsets cannot be changed at runtime)
1379
+ - A single server instance is reused for all clients
456
1380
  - Best for known, consistent tool requirements
457
1381
 
458
1382
  ## License