@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.
- package/README.md +123 -333
- package/dist/auth/handshake.d.ts +19 -4
- package/dist/auth/handshake.d.ts.map +1 -1
- package/dist/auth/handshake.js +52 -15
- package/dist/auth/handshake.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/delegation/did-key-resolver.d.ts.map +1 -1
- package/dist/delegation/did-key-resolver.js +9 -6
- package/dist/delegation/did-key-resolver.js.map +1 -1
- package/dist/delegation/outbound-headers.d.ts +2 -4
- package/dist/delegation/outbound-headers.d.ts.map +1 -1
- package/dist/delegation/outbound-headers.js +2 -3
- package/dist/delegation/outbound-headers.js.map +1 -1
- package/dist/delegation/statuslist-manager.d.ts.map +1 -1
- package/dist/delegation/statuslist-manager.js +1 -1
- package/dist/delegation/statuslist-manager.js.map +1 -1
- package/dist/delegation/vc-verifier.d.ts.map +1 -1
- package/dist/delegation/vc-verifier.js +2 -2
- package/dist/delegation/vc-verifier.js.map +1 -1
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/mcpi-transport.d.ts +39 -0
- package/dist/middleware/mcpi-transport.d.ts.map +1 -0
- package/dist/middleware/mcpi-transport.js +121 -0
- package/dist/middleware/mcpi-transport.js.map +1 -0
- package/dist/middleware/with-mcpi-server.d.ts +25 -9
- package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
- package/dist/middleware/with-mcpi-server.js +62 -47
- package/dist/middleware/with-mcpi-server.js.map +1 -1
- package/dist/middleware/with-mcpi.d.ts +26 -5
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +108 -10
- package/dist/middleware/with-mcpi.js.map +1 -1
- package/dist/providers/memory.js +2 -2
- package/dist/providers/memory.js.map +1 -1
- package/dist/session/manager.d.ts +7 -1
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +20 -4
- package/dist/session/manager.js.map +1 -1
- package/dist/utils/crypto-service.d.ts.map +1 -1
- package/dist/utils/crypto-service.js +11 -10
- package/dist/utils/crypto-service.js.map +1 -1
- package/dist/utils/did-helpers.d.ts +12 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +18 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/errors.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +1 -1
- package/src/__tests__/integration/mcp-enhance-server.test.ts +48 -5
- package/src/__tests__/integration/mcp-transport-context7.test.ts +19 -15
- package/src/__tests__/integration/mcp-transport.test.ts +13 -10
- package/src/__tests__/providers/base.test.ts +1 -1
- package/src/__tests__/providers/memory.test.ts +2 -2
- package/src/__tests__/utils/mock-providers.ts +2 -2
- package/src/auth/__tests__/handshake.test.ts +190 -0
- package/src/auth/handshake.ts +88 -21
- package/src/auth/index.ts +1 -0
- package/src/delegation/__tests__/did-key-resolver.test.ts +2 -2
- package/src/delegation/__tests__/outbound-headers.test.ts +16 -20
- package/src/delegation/__tests__/statuslist-manager.test.ts +120 -7
- package/src/delegation/__tests__/vc-verifier.test.ts +45 -3
- package/src/delegation/did-key-resolver.ts +11 -6
- package/src/delegation/outbound-headers.ts +1 -4
- package/src/delegation/statuslist-manager.ts +3 -1
- package/src/delegation/vc-verifier.ts +3 -2
- package/src/errors.ts +65 -0
- package/src/index.ts +10 -0
- package/src/middleware/__tests__/mcpi-transport.test.ts +150 -0
- package/src/middleware/__tests__/with-mcpi-server.test.ts +117 -0
- package/src/middleware/__tests__/with-mcpi.test.ts +124 -6
- package/src/middleware/index.ts +6 -0
- package/src/middleware/mcpi-transport.ts +162 -0
- package/src/middleware/with-mcpi-server.ts +83 -92
- package/src/middleware/with-mcpi.ts +147 -11
- package/src/proof/__tests__/errors.test.ts +79 -0
- package/src/proof/__tests__/verifier.test.ts +5 -5
- package/src/providers/memory.ts +2 -2
- package/src/session/__tests__/session-manager.test.ts +3 -3
- package/src/session/manager.ts +28 -6
- package/src/utils/crypto-service.ts +11 -10
- 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}
|
|
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
|
|
61
|
-
* Matches the public API
|
|
69
|
+
* Minimal McpServer interface — avoids hard dependency on @modelcontextprotocol/sdk.
|
|
70
|
+
* Matches the subset of McpServer's public API that withMCPI() uses.
|
|
62
71
|
*/
|
|
63
72
|
interface McpServerLike {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 `
|
|
77
|
-
* 3.
|
|
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
|
-
*
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
107
|
-
* In production, MCP-I-aware
|
|
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 +
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|