@mcp-i/core 1.1.0-canary.2 → 1.1.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 (94) hide show
  1. package/README.md +123 -333
  2. package/dist/auth/handshake.d.ts +19 -4
  3. package/dist/auth/handshake.d.ts.map +1 -1
  4. package/dist/auth/handshake.js +52 -15
  5. package/dist/auth/handshake.js.map +1 -1
  6. package/dist/auth/index.d.ts +1 -1
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js.map +1 -1
  9. package/dist/delegation/did-key-resolver.d.ts.map +1 -1
  10. package/dist/delegation/did-key-resolver.js +9 -6
  11. package/dist/delegation/did-key-resolver.js.map +1 -1
  12. package/dist/delegation/outbound-headers.d.ts +2 -4
  13. package/dist/delegation/outbound-headers.d.ts.map +1 -1
  14. package/dist/delegation/outbound-headers.js +2 -3
  15. package/dist/delegation/outbound-headers.js.map +1 -1
  16. package/dist/delegation/statuslist-manager.d.ts.map +1 -1
  17. package/dist/delegation/statuslist-manager.js +1 -1
  18. package/dist/delegation/statuslist-manager.js.map +1 -1
  19. package/dist/delegation/vc-verifier.d.ts.map +1 -1
  20. package/dist/delegation/vc-verifier.js +2 -2
  21. package/dist/delegation/vc-verifier.js.map +1 -1
  22. package/dist/errors.d.ts +42 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +45 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +1 -0
  31. package/dist/middleware/index.d.ts.map +1 -1
  32. package/dist/middleware/index.js +1 -0
  33. package/dist/middleware/index.js.map +1 -1
  34. package/dist/middleware/mcpi-transport.d.ts +39 -0
  35. package/dist/middleware/mcpi-transport.d.ts.map +1 -0
  36. package/dist/middleware/mcpi-transport.js +121 -0
  37. package/dist/middleware/mcpi-transport.js.map +1 -0
  38. package/dist/middleware/with-mcpi-server.d.ts +25 -9
  39. package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
  40. package/dist/middleware/with-mcpi-server.js +62 -47
  41. package/dist/middleware/with-mcpi-server.js.map +1 -1
  42. package/dist/middleware/with-mcpi.d.ts +26 -5
  43. package/dist/middleware/with-mcpi.d.ts.map +1 -1
  44. package/dist/middleware/with-mcpi.js +108 -10
  45. package/dist/middleware/with-mcpi.js.map +1 -1
  46. package/dist/providers/memory.js +2 -2
  47. package/dist/providers/memory.js.map +1 -1
  48. package/dist/session/manager.d.ts +7 -1
  49. package/dist/session/manager.d.ts.map +1 -1
  50. package/dist/session/manager.js +20 -4
  51. package/dist/session/manager.js.map +1 -1
  52. package/dist/utils/crypto-service.d.ts.map +1 -1
  53. package/dist/utils/crypto-service.js +11 -10
  54. package/dist/utils/crypto-service.js.map +1 -1
  55. package/dist/utils/did-helpers.d.ts +12 -0
  56. package/dist/utils/did-helpers.d.ts.map +1 -1
  57. package/dist/utils/did-helpers.js +18 -0
  58. package/dist/utils/did-helpers.js.map +1 -1
  59. package/package.json +3 -3
  60. package/src/__tests__/errors.test.ts +56 -0
  61. package/src/__tests__/integration/full-flow.test.ts +1 -1
  62. package/src/__tests__/integration/mcp-enhance-server.test.ts +48 -5
  63. package/src/__tests__/integration/mcp-transport-context7.test.ts +19 -15
  64. package/src/__tests__/integration/mcp-transport.test.ts +13 -10
  65. package/src/__tests__/providers/base.test.ts +1 -1
  66. package/src/__tests__/providers/memory.test.ts +2 -2
  67. package/src/__tests__/utils/mock-providers.ts +2 -2
  68. package/src/auth/__tests__/handshake.test.ts +190 -0
  69. package/src/auth/handshake.ts +88 -21
  70. package/src/auth/index.ts +1 -0
  71. package/src/delegation/__tests__/did-key-resolver.test.ts +2 -2
  72. package/src/delegation/__tests__/outbound-headers.test.ts +16 -20
  73. package/src/delegation/__tests__/statuslist-manager.test.ts +120 -7
  74. package/src/delegation/__tests__/vc-verifier.test.ts +45 -3
  75. package/src/delegation/did-key-resolver.ts +11 -6
  76. package/src/delegation/outbound-headers.ts +1 -4
  77. package/src/delegation/statuslist-manager.ts +3 -1
  78. package/src/delegation/vc-verifier.ts +3 -2
  79. package/src/errors.ts +65 -0
  80. package/src/index.ts +10 -0
  81. package/src/middleware/__tests__/mcpi-transport.test.ts +150 -0
  82. package/src/middleware/__tests__/with-mcpi-server.test.ts +117 -0
  83. package/src/middleware/__tests__/with-mcpi.test.ts +124 -6
  84. package/src/middleware/index.ts +6 -0
  85. package/src/middleware/mcpi-transport.ts +162 -0
  86. package/src/middleware/with-mcpi-server.ts +83 -92
  87. package/src/middleware/with-mcpi.ts +147 -11
  88. package/src/proof/__tests__/errors.test.ts +79 -0
  89. package/src/proof/__tests__/verifier.test.ts +5 -5
  90. package/src/providers/memory.ts +2 -2
  91. package/src/session/__tests__/session-manager.test.ts +3 -3
  92. package/src/session/manager.ts +28 -6
  93. package/src/utils/crypto-service.ts +11 -10
  94. package/src/utils/did-helpers.ts +19 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * MCPITransport — Proof-injecting Transport Wrapper
3
+ *
4
+ * Wraps any MCP Transport to intercept `tools/call` responses and attach
5
+ * MCP-I detached proofs. Uses only the public Transport interface — no
6
+ * private SDK internals accessed.
7
+ *
8
+ * The McpServer never knows this wrapper exists. It sees a normal transport.
9
+ * The connected client sees normal MCP responses with an added `_meta.proof`.
10
+ *
11
+ * How it works:
12
+ * 1. Incoming `tools/call` requests are captured (by id) to record tool
13
+ * name and arguments for proof generation.
14
+ * 2. Outgoing responses for those ids get a proof injected into `_meta`.
15
+ * 3. All other message types pass through unmodified.
16
+ *
17
+ * @module mcpi-transport
18
+ */
19
+
20
+ import type { MCPIMiddleware, MCPIToolHandler } from "./with-mcpi.js";
21
+ import { logger } from "../logging/index.js";
22
+
23
+ /** Minimal Transport interface — matches @modelcontextprotocol/sdk Transport */
24
+ export interface Transport {
25
+ start(): Promise<void>;
26
+ send(message: JSONRPCMessage): Promise<void>;
27
+ close(): Promise<void>;
28
+ onmessage?: (message: JSONRPCMessage) => void;
29
+ onclose?: () => void;
30
+ onerror?: (error: Error) => void;
31
+ }
32
+
33
+ export type JSONRPCMessage = Record<string, unknown>;
34
+
35
+ interface PendingCall {
36
+ toolName: string;
37
+ args: Record<string, unknown>;
38
+ }
39
+
40
+ type ToolResult = {
41
+ content: Array<{ type: string; text: string; [key: string]: unknown }>;
42
+ isError?: boolean;
43
+ [key: string]: unknown;
44
+ };
45
+
46
+ /**
47
+ * Creates a transport wrapper that injects MCP-I proofs into `tools/call`
48
+ * responses.
49
+ *
50
+ * @param inner - The real transport (Stdio, HTTP, etc.)
51
+ * @param mcpi - Configured MCPIMiddleware instance
52
+ * @param exclude - Tool names to skip proof generation for
53
+ */
54
+ export function createMCPITransport(
55
+ inner: Transport,
56
+ mcpi: MCPIMiddleware,
57
+ exclude: string[] = ["_mcpi", "_mcpi_handshake"],
58
+ ): Transport {
59
+ // Request id → { toolName, args } for pending tool calls
60
+ const pending = new Map<unknown, PendingCall>();
61
+
62
+ const wrapper: Transport = {
63
+ start: () => inner.start(),
64
+ close: () => inner.close(),
65
+
66
+ // McpServer writes into wrapper.onmessage — forward to inner so the
67
+ // real transport can drive it.
68
+ set onmessage(handler: ((msg: JSONRPCMessage) => void) | undefined) {
69
+ inner.onmessage = handler;
70
+ },
71
+ get onmessage() {
72
+ return inner.onmessage;
73
+ },
74
+
75
+ set onclose(handler: (() => void) | undefined) {
76
+ inner.onclose = handler;
77
+ },
78
+ get onclose() {
79
+ return inner.onclose;
80
+ },
81
+
82
+ set onerror(handler: ((err: Error) => void) | undefined) {
83
+ inner.onerror = handler;
84
+ },
85
+ get onerror() {
86
+ return inner.onerror;
87
+ },
88
+
89
+ // McpServer calls send() for every outgoing message.
90
+ // Intercept tools/call responses here to inject proofs.
91
+ async send(message: JSONRPCMessage): Promise<void> {
92
+ const id = message.id;
93
+ const call = id !== undefined ? pending.get(id) : undefined;
94
+
95
+ if (call) {
96
+ pending.delete(id);
97
+ try {
98
+ const rawResult = message.result as ToolResult | undefined;
99
+ if (rawResult && !rawResult.isError) {
100
+ const handler: MCPIToolHandler = async () => rawResult;
101
+ const addProof = mcpi.wrapWithProof(call.toolName, handler);
102
+ const proofed = await addProof(call.args);
103
+ if (proofed._meta !== undefined) {
104
+ message = {
105
+ ...message,
106
+ result: proofed,
107
+ };
108
+ }
109
+ }
110
+ } catch (error) {
111
+ logger.error("[mcpi-transport] Proof injection failed", {
112
+ tool: call.toolName,
113
+ error: error instanceof Error ? error.message : String(error),
114
+ });
115
+ const rawResult = message.result as ToolResult | undefined;
116
+ if (rawResult) {
117
+ rawResult._meta = {
118
+ proofError: "Proof generation failed — response is unproven",
119
+ };
120
+ message = { ...message, result: rawResult };
121
+ }
122
+ }
123
+ }
124
+
125
+ return inner.send(message);
126
+ },
127
+ };
128
+
129
+ // Intercept incoming messages from the real transport to capture
130
+ // tools/call requests before McpServer processes them.
131
+ // We defer setting inner.onmessage until McpServer has set wrapper.onmessage
132
+ // via server.connect() — so we proxy through the getter/setter above and
133
+ // add our interception in a one-time initializer on start().
134
+ const originalStart = inner.start.bind(inner);
135
+ wrapper.start = async () => {
136
+ await originalStart();
137
+ // At this point McpServer has called server.connect(wrapper) which set
138
+ // wrapper.onmessage = <McpServer handler>. That assignment forwarded to
139
+ // inner.onmessage via the setter above. Now we inject our interceptor.
140
+ const downstream = inner.onmessage;
141
+ inner.onmessage = (message: JSONRPCMessage) => {
142
+ if (
143
+ message.method === "tools/call" &&
144
+ message.id !== undefined
145
+ ) {
146
+ const params = message.params as
147
+ | { name?: string; arguments?: Record<string, unknown> }
148
+ | undefined;
149
+ const toolName = params?.name;
150
+ if (toolName && !exclude.includes(toolName)) {
151
+ pending.set(message.id, {
152
+ toolName,
153
+ args: params?.arguments ?? {},
154
+ });
155
+ }
156
+ }
157
+ downstream?.(message);
158
+ };
159
+ };
160
+
161
+ return wrapper;
162
+ }
@@ -8,16 +8,19 @@
8
8
  * import { withMCPI } from '@mcp-i/core/middleware';
9
9
  * const mcpi = await withMCPI(server, { crypto: new NodeCryptoProvider() });
10
10
  * // All tools registered on `server` now get proofs automatically.
11
+ * await server.connect(transport); // transport is transparently wrapped
11
12
  */
12
13
 
13
14
  import type { CryptoProvider } from "../providers/base.js";
14
- import { generateDidKeyFromBase64 } from "../utils/did-helpers.js";
15
+ import { generateDidKeyFromBase64, didKeyFragment } from "../utils/did-helpers.js";
15
16
  import {
17
+ MCPI_ACTIONS,
16
18
  createMCPIMiddleware,
17
19
  type MCPIIdentityConfig,
18
20
  type MCPIDelegationConfig,
19
21
  type MCPIMiddleware,
20
22
  } from "./with-mcpi.js";
23
+ import { createMCPITransport, type Transport } from "./mcpi-transport.js";
21
24
  import { z } from "zod";
22
25
 
23
26
  export interface WithMCPIOptions {
@@ -35,6 +38,12 @@ export interface WithMCPIOptions {
35
38
  excludeTools?: string[];
36
39
  /** Delegation verification config */
37
40
  delegation?: MCPIDelegationConfig;
41
+ /**
42
+ * How the MCP-I protocol tool is exposed on the server.
43
+ * - "tool" (default): auto-register `_mcpi`
44
+ * - "none": do not register MCP-I tool (use middleware APIs for custom runtime hooks)
45
+ */
46
+ handshakeExposure?: "tool" | "none";
38
47
  }
39
48
 
40
49
  /**
@@ -50,33 +59,43 @@ export async function generateIdentity(
50
59
  const did = generateDidKeyFromBase64(keyPair.publicKey);
51
60
  return {
52
61
  did,
53
- kid: `${did}#keys-1`,
62
+ kid: `${did}#${didKeyFragment(did)}`,
54
63
  privateKey: keyPair.privateKey,
55
64
  publicKey: keyPair.publicKey,
56
65
  };
57
66
  }
58
67
 
59
68
  /**
60
- * McpServer typeminimal interface to avoid hard dependency on the SDK.
61
- * Matches the public API of @modelcontextprotocol/sdk McpServer.
69
+ * Minimal McpServer interfaceavoids hard dependency on @modelcontextprotocol/sdk.
70
+ * Matches the subset of McpServer's public API that withMCPI() uses.
62
71
  */
63
72
  interface McpServerLike {
64
- server: {
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- [key: string]: any;
67
- };
68
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
- registerTool(...args: any[]): void;
73
+ connect(transport: Transport): Promise<unknown>;
74
+ registerTool(
75
+ name: string,
76
+ config: Record<string, unknown>,
77
+ handler: (args: unknown) => Promise<unknown>,
78
+ ): void;
70
79
  }
71
80
 
72
81
  /**
73
82
  * Add MCP-I to a McpServer instance.
74
83
  *
75
84
  * 1. Auto-generates Ed25519 identity (or uses provided one)
76
- * 2. Registers `_mcpi_handshake` tool
77
- * 3. Intercepts the `tools/call` request handler to auto-attach proofs
85
+ * 2. Registers `_mcpi` tool by default (`handshakeExposure: "tool"`)
86
+ * 3. Patches `server.connect()` to transparently wrap the transport with
87
+ * MCPITransport, which injects detached proofs into all `tools/call`
88
+ * responses using only the public Transport interface.
89
+ *
90
+ * The user-facing API is unchanged — register tools before or after this
91
+ * call, then connect as normal:
78
92
  *
79
- * @param server - McpServer instance
93
+ * ```ts
94
+ * const mcpi = await withMCPI(server, { crypto: new NodeCryptoProvider() });
95
+ * await server.connect(transport); // MCPITransport wraps silently
96
+ * ```
97
+ *
98
+ * @param server - McpServer instance
80
99
  * @param options - Configuration
81
100
  * @returns The MCPIMiddleware instance for advanced usage (wrapWithDelegation, etc.)
82
101
  */
@@ -97,88 +116,60 @@ export async function withMCPI(
97
116
  options.crypto,
98
117
  );
99
118
 
100
- // Register _mcpi_handshake tool
101
- server.registerTool(
102
- "_mcpi_handshake",
103
- {
104
- description:
105
- "MCP-I identity handshake — establishes a cryptographic session",
106
- inputSchema: {
107
- nonce: z.string().describe("Client-generated unique nonce"),
108
- audience: z
109
- .string()
110
- .describe("Intended audience (server DID or URL)"),
111
- timestamp: z.number().describe("Unix epoch seconds"),
119
+ if ((options.handshakeExposure ?? "tool") === "tool") {
120
+ // Register the unified _mcpi tool for protocol operations.
121
+ server.registerTool(
122
+ "_mcpi",
123
+ {
124
+ description:
125
+ "MCP-I protocol — identity verification, session handshake, and server metadata",
126
+ annotations: { title: "MCP-I Protocol", readOnlyHint: true },
127
+ inputSchema: {
128
+ action: z
129
+ .enum(MCPI_ACTIONS)
130
+ .describe("Protocol operation to perform"),
131
+ nonce: z
132
+ .string()
133
+ .optional()
134
+ .describe("Client-generated unique nonce (handshake)"),
135
+ audience: z
136
+ .string()
137
+ .optional()
138
+ .describe("Intended audience (handshake)"),
139
+ timestamp: z
140
+ .number()
141
+ .optional()
142
+ .describe("Unix epoch seconds (handshake)"),
143
+ agentDid: z
144
+ .string()
145
+ .optional()
146
+ .describe("Client agent DID (handshake, optional)"),
147
+ },
112
148
  },
113
- },
114
- async (args: unknown) => {
115
- const result = await mcpi.handleHandshake(args as Record<string, unknown>);
116
- return {
117
- ...result,
118
- content: result.content.map((c) => ({ ...c, type: "text" as const })),
119
- };
120
- },
121
- );
122
-
123
- // Auto-proof interception: wrap the tools/call handler
124
- const proofAllTools = options.proofAllTools ?? true;
125
-
126
- if (proofAllTools) {
127
- const lowLevel = server.server;
128
- const handlers: Map<string, Function> =
129
- lowLevel._requestHandlers;
130
- const original = handlers.get("tools/call");
131
-
132
- if (original) {
133
- handlers.set(
134
- "tools/call",
135
- async (request: Record<string, unknown>, extra: unknown) => {
136
- const result = (await (
137
- original as (
138
- req: Record<string, unknown>,
139
- ext: unknown,
140
- ) => Promise<Record<string, unknown>>
141
- )(request, extra)) as {
142
- content?: Array<{
143
- type: string;
144
- text: string;
145
- [key: string]: unknown;
146
- }>;
147
- isError?: boolean;
148
- _meta?: Record<string, unknown>;
149
- [key: string]: unknown;
150
- };
151
-
152
- const params = request.params as
153
- | { name?: string; arguments?: Record<string, unknown> }
154
- | undefined;
155
- const toolName = params?.name;
156
-
157
- if (
158
- !toolName ||
159
- toolName === "_mcpi_handshake" ||
160
- result.isError
161
- ) {
162
- return result;
163
- }
149
+ async (args: unknown) => {
150
+ const result = await mcpi.handleMCPI(
151
+ args as Record<string, unknown>,
152
+ );
153
+ return {
154
+ ...result,
155
+ content: result.content.map((c) => ({ ...c, type: "text" as const })),
156
+ };
157
+ },
158
+ );
159
+ }
164
160
 
165
- if (options.excludeTools?.includes(toolName)) {
166
- return result;
167
- }
161
+ // Auto-proof interception via transport wrapper (public API only).
162
+ //
163
+ // We patch server.connect() so that whatever transport the caller passes
164
+ // is silently wrapped with MCPITransport before McpServer sees it.
165
+ // Tool registration order does not matter — proofs are injected at the
166
+ // transport boundary, after McpServer has already dispatched the call.
167
+ if (options.proofAllTools !== false) {
168
+ const exclude = ["_mcpi", "_mcpi_handshake", ...(options.excludeTools ?? [])];
169
+ const originalConnect = server.connect.bind(server);
168
170
 
169
- // Use wrapWithProof to add proof — it handles session management
170
- const addProof = mcpi.wrapWithProof(
171
- toolName,
172
- async () => result as {
173
- content: Array<{ type: string; text: string; [key: string]: unknown }>;
174
- isError?: boolean;
175
- [key: string]: unknown;
176
- },
177
- );
178
- return addProof(params?.arguments ?? {});
179
- },
180
- );
181
- }
171
+ server.connect = (transport: Transport) =>
172
+ originalConnect(createMCPITransport(transport, mcpi, exclude));
182
173
  }
183
174
 
184
175
  return mcpi;
@@ -4,8 +4,8 @@
4
4
  * Adds identity, session management, and proof generation to MCP servers.
5
5
  *
6
6
  * For most use cases, prefer the high-level `withMCPI()` adapter from
7
- * `./with-mcpi-server.ts` which auto-registers the handshake tool and
8
- * auto-attaches proofs to all tool responses:
7
+ * `./with-mcpi-server.ts` which (by default) auto-registers the handshake
8
+ * tool and auto-attaches proofs to all tool responses:
9
9
  *
10
10
  * import { withMCPI } from '@mcp-i/core';
11
11
  * await withMCPI(server, { crypto: new NodeCryptoProvider() });
@@ -47,6 +47,7 @@ import {
47
47
  type DelegationRecord,
48
48
  } from "../types/protocol.js";
49
49
  import { logger } from "../logging/index.js";
50
+ import { MCPI_ERROR_CODES } from "../errors.js";
50
51
  import { canonicalizeJSON } from "../delegation/utils.js";
51
52
  import { base64urlDecodeToBytes, base64urlEncodeFromBytes, bytesToBase64 } from "../utils/base64.js";
52
53
 
@@ -55,8 +56,12 @@ export interface MCPIIdentityConfig {
55
56
  kid: string;
56
57
  privateKey: string;
57
58
  publicKey: string;
59
+ agentName?: string;
58
60
  }
59
61
 
62
+ export const MCPI_ACTIONS = ["handshake", "identity", "reputation"] as const;
63
+ type MCPIAction = (typeof MCPI_ACTIONS)[number];
64
+
60
65
  export interface MCPIDelegationConfig {
61
66
  /**
62
67
  * Optional custom DID resolver. If it returns null, middleware falls back to
@@ -103,8 +108,8 @@ export interface MCPIConfig {
103
108
  /**
104
109
  * When true, automatically creates a session on the first tool call
105
110
  * if no session exists. Useful for demos and development where
106
- * MCP clients don't support the _mcpi_handshake flow.
107
- * In production, MCP-I-aware clients handle handshake automatically.
111
+ * MCP clients don't support the `_mcpi` handshake flow.
112
+ * In production, MCP-I-aware runtimes should execute handshake before tool calls.
108
113
  */
109
114
  autoSession?: boolean;
110
115
  }
@@ -155,12 +160,29 @@ export interface MCPIMiddleware {
155
160
  proofGenerator: ProofGenerator;
156
161
 
157
162
  /**
163
+ * Unified tool definition for `_mcpi`.
164
+ * Include this in your ListToolsRequest handler's tool list.
165
+ */
166
+ mcpiTool: MCPIToolDefinition;
167
+
168
+ /**
169
+ * @deprecated Use `mcpiTool` (`_mcpi` with `action: "handshake"`).
158
170
  * Tool definition for `_mcpi_handshake`.
159
171
  * Include this in your ListToolsRequest handler's tool list.
160
172
  */
161
173
  handshakeTool: MCPIToolDefinition;
162
174
 
163
175
  /**
176
+ * Handle a unified `_mcpi` action. Use this in your CallToolRequest handler
177
+ * when `request.params.name === '_mcpi'`.
178
+ */
179
+ handleMCPI(args: Record<string, unknown>): Promise<{
180
+ content: Array<{ type: string; text: string }>;
181
+ isError?: boolean;
182
+ }>;
183
+
184
+ /**
185
+ * @deprecated Use `handleMCPI` with `action: "handshake"`.
164
186
  * Handle a handshake call. Use this in your CallToolRequest handler
165
187
  * when `request.params.name === '_mcpi_handshake'`.
166
188
  */
@@ -267,7 +289,8 @@ function validateScopeAttenuation(
267
289
  * Create MCP-I middleware for a standard MCP SDK Server.
268
290
  *
269
291
  * For most use cases, prefer {@link withMCPI} from `./with-mcpi-server.ts`
270
- * which wraps this function and auto-registers handshake + auto-attaches proofs.
292
+ * which wraps this function and (by default) auto-registers handshake +
293
+ * auto-attaches proofs.
271
294
  *
272
295
  * Use `createMCPIMiddleware` directly when:
273
296
  * - You use the low-level `Server` API (not `McpServer`)
@@ -331,6 +354,33 @@ export function createMCPIMiddleware(
331
354
  },
332
355
  };
333
356
 
357
+ const mcpiTool: MCPIToolDefinition = {
358
+ name: "_mcpi",
359
+ description:
360
+ "MCP-I protocol — identity verification, session handshake, and server metadata",
361
+ inputSchema: {
362
+ type: "object",
363
+ properties: {
364
+ action: {
365
+ type: "string",
366
+ enum: [...MCPI_ACTIONS],
367
+ description: "Protocol operation to perform",
368
+ },
369
+ nonce: { type: "string", description: "Client-generated unique nonce" },
370
+ audience: {
371
+ type: "string",
372
+ description: "Intended audience (server DID or URL)",
373
+ },
374
+ timestamp: { type: "number", description: "Unix epoch seconds" },
375
+ agentDid: {
376
+ type: "string",
377
+ description: "Client agent DID (optional)",
378
+ },
379
+ },
380
+ required: ["action"],
381
+ },
382
+ };
383
+
334
384
  async function handleHandshake(args: Record<string, unknown>): Promise<{
335
385
  content: Array<{ type: string; text: string }>;
336
386
  isError?: boolean;
@@ -343,7 +393,7 @@ export function createMCPIMiddleware(
343
393
  text: JSON.stringify({
344
394
  success: false,
345
395
  error: {
346
- code: "MCPI_INVALID_HANDSHAKE",
396
+ code: MCPI_ERROR_CODES.handshake_failed,
347
397
  message:
348
398
  "Invalid handshake format: requires nonce (string), audience (string), and timestamp (positive integer)",
349
399
  },
@@ -381,9 +431,82 @@ export function createMCPIMiddleware(
381
431
  };
382
432
  }
383
433
 
434
+ async function handleIdentity(): Promise<{
435
+ content: Array<{ type: string; text: string }>;
436
+ isError?: boolean;
437
+ }> {
438
+ return {
439
+ content: [
440
+ {
441
+ type: "text",
442
+ text: JSON.stringify({
443
+ did: identity.did,
444
+ kid: identity.kid,
445
+ name: config.identity.agentName ?? identity.did,
446
+ capabilities: ["handshake", "signing", "verification"],
447
+ protocolVersion: "1.0.0",
448
+ }),
449
+ },
450
+ ],
451
+ };
452
+ }
453
+
454
+ async function handleMCPI(args: Record<string, unknown>): Promise<{
455
+ content: Array<{ type: string; text: string }>;
456
+ isError?: boolean;
457
+ }> {
458
+ const action =
459
+ typeof args.action === "string"
460
+ ? (args.action as MCPIAction)
461
+ : undefined;
462
+
463
+ switch (action) {
464
+ case "handshake":
465
+ return handleHandshake(args);
466
+
467
+ case "identity":
468
+ return handleIdentity();
469
+
470
+ case "reputation":
471
+ return {
472
+ content: [
473
+ {
474
+ type: "text",
475
+ text: JSON.stringify({
476
+ success: false,
477
+ error: {
478
+ code: MCPI_ERROR_CODES.runtime_error,
479
+ message:
480
+ 'action: "reputation" is not yet implemented.',
481
+ },
482
+ }),
483
+ },
484
+ ],
485
+ isError: true,
486
+ };
487
+
488
+ default:
489
+ return {
490
+ content: [
491
+ {
492
+ type: "text",
493
+ text: JSON.stringify({
494
+ success: false,
495
+ error: {
496
+ code: MCPI_ERROR_CODES.invalid_request,
497
+ message: `Unknown _mcpi action: "${action ?? "(missing)"}". Valid actions: ${MCPI_ACTIONS.join(", ")}`,
498
+ },
499
+ }),
500
+ },
501
+ ],
502
+ isError: true,
503
+ };
504
+ }
505
+ }
506
+
384
507
  /**
385
508
  * Auto-create a session for proof generation when no handshake has occurred.
386
- * In production, MCP-I-aware clients handle the handshake automatically.
509
+ * In production, MCP-I-aware runtimes should execute handshake before tool calls.
387
510
  * This convenience mode allows non-MCP-I clients (like MCP Inspector) to
388
511
  * still see proofs without manual handshake.
389
512
  */
@@ -449,8 +572,14 @@ export function createMCPIMiddleware(
449
572
 
450
573
  // Attach proof as _meta (rendered by MCP Inspector, invisible to LLMs)
451
574
  result._meta = { proof };
452
- } catch {
453
- // Proof generation failure is non-fatal — the tool result is still valid
575
+ } catch (error) {
576
+ logger.error("[mcpi] Proof generation failed", {
577
+ tool: toolName,
578
+ error: error instanceof Error ? error.message : String(error),
579
+ });
580
+ result._meta = {
581
+ proofError: "Proof generation failed — response is unproven",
582
+ };
454
583
  }
455
584
 
456
585
  return result;
@@ -638,8 +767,13 @@ export function createMCPIMiddleware(
638
767
  }
639
768
  }
640
769
 
770
+ const skipStatusForLegacy =
771
+ legacyUnsafeDelegationEnabled &&
772
+ !!credential.credentialStatus &&
773
+ !delegationConfig?.statusListResolver;
641
774
  const credentialVerification = await verifier.verifyDelegationCredential(
642
775
  credential,
776
+ skipStatusForLegacy ? { skipStatus: true } : undefined,
643
777
  );
644
778
  if (!credentialVerification.valid) {
645
779
  return {
@@ -747,7 +881,7 @@ export function createMCPIMiddleware(
747
881
  `[mcpi] Delegation verification failed for "${toolName}": ${verificationResult.reason}`,
748
882
  );
749
883
  return buildDelegationErrorResponse(
750
- "delegation_invalid",
884
+ MCPI_ERROR_CODES.delegation_invalid,
751
885
  verificationResult.reason ?? "Unknown delegation validation error",
752
886
  );
753
887
  }
@@ -758,7 +892,7 @@ export function createMCPIMiddleware(
758
892
  `[mcpi] Delegation missing required scope "${config.scopeId}" for "${toolName}"`,
759
893
  );
760
894
  return buildDelegationErrorResponse(
761
- "delegation_scope_missing",
895
+ MCPI_ERROR_CODES.insufficient_scope,
762
896
  `Required scope "${config.scopeId}" not in delegation scopes`,
763
897
  );
764
898
  }
@@ -780,7 +914,9 @@ export function createMCPIMiddleware(
780
914
  identity: config.identity,
781
915
  sessionManager,
782
916
  proofGenerator,
917
+ mcpiTool,
783
918
  handshakeTool,
919
+ handleMCPI,
784
920
  handleHandshake,
785
921
  wrapWithProof,
786
922
  wrapWithDelegation,