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