@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
package/src/errors.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-I Canonical Error Codes
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all wire-format error codes.
|
|
5
|
+
* Aligned with the error catalog at modelcontextprotocol-identity.io.
|
|
6
|
+
*
|
|
7
|
+
* Naming convention: snake_case, no protocol prefix.
|
|
8
|
+
* Follows OAuth 2.0 / Stripe conventions for readability and portability.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const MCPI_ERROR_CODES = {
|
|
12
|
+
// Proof errors
|
|
13
|
+
invalid_proof: "invalid_proof",
|
|
14
|
+
invalid_jws: "invalid_jws",
|
|
15
|
+
nonce_replay: "nonce_replay",
|
|
16
|
+
timestamp_skew: "timestamp_skew",
|
|
17
|
+
|
|
18
|
+
// Identity / DID errors
|
|
19
|
+
did_not_found: "did_not_found",
|
|
20
|
+
invalid_public_key: "invalid_public_key",
|
|
21
|
+
|
|
22
|
+
// Session / Handshake errors
|
|
23
|
+
handshake_failed: "handshake_failed",
|
|
24
|
+
session_expired: "session_expired",
|
|
25
|
+
invalid_request: "invalid_request",
|
|
26
|
+
|
|
27
|
+
// Delegation errors
|
|
28
|
+
needs_authorization: "needs_authorization",
|
|
29
|
+
insufficient_scope: "insufficient_scope",
|
|
30
|
+
delegation_expired: "delegation_expired",
|
|
31
|
+
delegation_not_yet_valid: "delegation_not_yet_valid",
|
|
32
|
+
delegation_revoked: "delegation_revoked",
|
|
33
|
+
delegation_invalid: "delegation_invalid",
|
|
34
|
+
budget_exceeded: "budget_exceeded",
|
|
35
|
+
rate_limit_exceeded: "rate_limit_exceeded",
|
|
36
|
+
|
|
37
|
+
// Token errors
|
|
38
|
+
invalid_token: "invalid_token",
|
|
39
|
+
token_expired: "token_expired",
|
|
40
|
+
|
|
41
|
+
// Registry errors
|
|
42
|
+
mirror_pending: "mirror_pending",
|
|
43
|
+
claim_failed: "claim_failed",
|
|
44
|
+
|
|
45
|
+
// System errors
|
|
46
|
+
configuration_error: "configuration_error",
|
|
47
|
+
runtime_error: "runtime_error",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export type MCPIErrorCode =
|
|
51
|
+
(typeof MCPI_ERROR_CODES)[keyof typeof MCPI_ERROR_CODES];
|
|
52
|
+
|
|
53
|
+
export interface MCPIErrorResponse {
|
|
54
|
+
code: MCPIErrorCode;
|
|
55
|
+
message: string;
|
|
56
|
+
details?: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createMCPIError(
|
|
60
|
+
code: MCPIErrorCode,
|
|
61
|
+
message: string,
|
|
62
|
+
details?: Record<string, unknown>,
|
|
63
|
+
): MCPIErrorResponse {
|
|
64
|
+
return details ? { code, message, details } : { code, message };
|
|
65
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
* This ensures callers can always handle failures without try/catch on validation paths.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
// Error contract
|
|
19
|
+
export {
|
|
20
|
+
MCPI_ERROR_CODES,
|
|
21
|
+
createMCPIError,
|
|
22
|
+
type MCPIErrorCode,
|
|
23
|
+
type MCPIErrorResponse,
|
|
24
|
+
} from './errors.js';
|
|
25
|
+
|
|
18
26
|
// Protocol types
|
|
19
27
|
export type {
|
|
20
28
|
DelegationConstraints,
|
|
@@ -171,6 +179,7 @@ export {
|
|
|
171
179
|
extractAgentSlug,
|
|
172
180
|
generateDidKeyFromBytes,
|
|
173
181
|
generateDidKeyFromBase64,
|
|
182
|
+
didKeyFragment,
|
|
174
183
|
} from './utils/did-helpers.js';
|
|
175
184
|
|
|
176
185
|
// Auth module
|
|
@@ -182,6 +191,7 @@ export {
|
|
|
182
191
|
type VerifyOrHintsResult,
|
|
183
192
|
type AgentReputation,
|
|
184
193
|
type ResumeTokenStore,
|
|
194
|
+
type UnknownAgentPolicy,
|
|
185
195
|
type DelegationVerifier,
|
|
186
196
|
type VerifyDelegationResult,
|
|
187
197
|
} from './auth/index.js';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { createMCPITransport, type Transport, type JSONRPCMessage } from "../mcpi-transport.js";
|
|
3
|
+
import type { MCPIMiddleware, MCPIToolHandler } from "../with-mcpi.js";
|
|
4
|
+
|
|
5
|
+
function createMockTransport(): Transport & { sentMessages: JSONRPCMessage[] } {
|
|
6
|
+
const sent: JSONRPCMessage[] = [];
|
|
7
|
+
return {
|
|
8
|
+
sentMessages: sent,
|
|
9
|
+
start: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
send: vi.fn(async (msg: JSONRPCMessage) => { sent.push(msg); }),
|
|
11
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
onmessage: undefined,
|
|
13
|
+
onclose: undefined,
|
|
14
|
+
onerror: undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createMockMCPI(proofResult?: Record<string, unknown>): MCPIMiddleware {
|
|
19
|
+
return {
|
|
20
|
+
wrapWithProof: (_toolName: string, handler: MCPIToolHandler) => {
|
|
21
|
+
return async (args: Record<string, unknown>) => {
|
|
22
|
+
const result = await handler(args);
|
|
23
|
+
if (proofResult) {
|
|
24
|
+
result._meta = { proof: proofResult };
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
} as unknown as MCPIMiddleware;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("createMCPITransport", () => {
|
|
33
|
+
it("should pass through non-tools/call messages unmodified", async () => {
|
|
34
|
+
const inner = createMockTransport();
|
|
35
|
+
const mcpi = createMockMCPI();
|
|
36
|
+
const wrapper = createMCPITransport(inner, mcpi);
|
|
37
|
+
|
|
38
|
+
await wrapper.send({ jsonrpc: "2.0", method: "resources/list", id: 1 });
|
|
39
|
+
|
|
40
|
+
expect(inner.sentMessages).toHaveLength(1);
|
|
41
|
+
expect(inner.sentMessages[0]).toEqual({ jsonrpc: "2.0", method: "resources/list", id: 1 });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should skip proof injection for excluded tools", async () => {
|
|
45
|
+
const inner = createMockTransport();
|
|
46
|
+
const mcpi = createMockMCPI({ jws: "test" });
|
|
47
|
+
const wrapper = createMCPITransport(inner, mcpi, ["_mcpi"]);
|
|
48
|
+
|
|
49
|
+
await wrapper.start();
|
|
50
|
+
|
|
51
|
+
// Simulate incoming _mcpi request
|
|
52
|
+
inner.onmessage!({
|
|
53
|
+
jsonrpc: "2.0",
|
|
54
|
+
method: "tools/call",
|
|
55
|
+
id: 42,
|
|
56
|
+
params: { name: "_mcpi", arguments: { action: "handshake" } },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Simulate response
|
|
60
|
+
await wrapper.send({
|
|
61
|
+
jsonrpc: "2.0",
|
|
62
|
+
id: 42,
|
|
63
|
+
result: { content: [{ type: "text", text: "ok" }] },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Should pass through without proof
|
|
67
|
+
const sent = inner.sentMessages[0] as { result?: { _meta?: unknown } };
|
|
68
|
+
expect(sent.result?._meta).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should inject proof for non-excluded tool calls", async () => {
|
|
72
|
+
const inner = createMockTransport();
|
|
73
|
+
const proof = { jws: "test.jws.sig", meta: { did: "did:key:z6Mk..." } };
|
|
74
|
+
const mcpi = createMockMCPI(proof);
|
|
75
|
+
const wrapper = createMCPITransport(inner, mcpi);
|
|
76
|
+
|
|
77
|
+
await wrapper.start();
|
|
78
|
+
|
|
79
|
+
// Simulate incoming greet request
|
|
80
|
+
inner.onmessage!({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
method: "tools/call",
|
|
83
|
+
id: 1,
|
|
84
|
+
params: { name: "greet", arguments: { name: "test" } },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Simulate response
|
|
88
|
+
await wrapper.send({
|
|
89
|
+
jsonrpc: "2.0",
|
|
90
|
+
id: 1,
|
|
91
|
+
result: { content: [{ type: "text", text: "Hello!" }] },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const sent = inner.sentMessages[0] as { result?: { _meta?: { proof?: unknown } } };
|
|
95
|
+
expect(sent.result?._meta?.proof).toEqual(proof);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should not inject proof for error responses", async () => {
|
|
99
|
+
const inner = createMockTransport();
|
|
100
|
+
const mcpi = createMockMCPI({ jws: "test" });
|
|
101
|
+
const wrapper = createMCPITransport(inner, mcpi);
|
|
102
|
+
|
|
103
|
+
await wrapper.start();
|
|
104
|
+
|
|
105
|
+
inner.onmessage!({
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
method: "tools/call",
|
|
108
|
+
id: 1,
|
|
109
|
+
params: { name: "greet", arguments: {} },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await wrapper.send({
|
|
113
|
+
jsonrpc: "2.0",
|
|
114
|
+
id: 1,
|
|
115
|
+
result: { content: [{ type: "text", text: "error" }], isError: true },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const sent = inner.sentMessages[0] as { result?: { _meta?: unknown } };
|
|
119
|
+
expect(sent.result?._meta).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should proxy onmessage/onclose/onerror to inner transport", () => {
|
|
123
|
+
const inner = createMockTransport();
|
|
124
|
+
const mcpi = createMockMCPI();
|
|
125
|
+
const wrapper = createMCPITransport(inner, mcpi);
|
|
126
|
+
|
|
127
|
+
const handler = () => {};
|
|
128
|
+
wrapper.onmessage = handler;
|
|
129
|
+
expect(inner.onmessage).toBe(handler);
|
|
130
|
+
expect(wrapper.onmessage).toBe(handler);
|
|
131
|
+
|
|
132
|
+
const closeHandler = () => {};
|
|
133
|
+
wrapper.onclose = closeHandler;
|
|
134
|
+
expect(inner.onclose).toBe(closeHandler);
|
|
135
|
+
|
|
136
|
+
const errorHandler = () => {};
|
|
137
|
+
wrapper.onerror = errorHandler;
|
|
138
|
+
expect(inner.onerror).toBe(errorHandler);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should delegate start and close to inner transport", async () => {
|
|
142
|
+
const inner = createMockTransport();
|
|
143
|
+
const mcpi = createMockMCPI();
|
|
144
|
+
const wrapper = createMCPITransport(inner, mcpi);
|
|
145
|
+
|
|
146
|
+
// close delegates directly
|
|
147
|
+
await wrapper.close();
|
|
148
|
+
expect(inner.close).toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { generateIdentity, withMCPI } from "../with-mcpi-server.js";
|
|
3
|
+
import { NodeCryptoProvider } from "../../__tests__/utils/node-crypto-provider.js";
|
|
4
|
+
|
|
5
|
+
const crypto = new NodeCryptoProvider();
|
|
6
|
+
|
|
7
|
+
describe("generateIdentity", () => {
|
|
8
|
+
it("should return did, kid, privateKey, and publicKey", async () => {
|
|
9
|
+
const identity = await generateIdentity(crypto);
|
|
10
|
+
|
|
11
|
+
expect(identity.did).toMatch(/^did:key:z6Mk/);
|
|
12
|
+
expect(identity.kid).toMatch(/^did:key:z6Mk.+#z6Mk/);
|
|
13
|
+
expect(identity.privateKey).toBeDefined();
|
|
14
|
+
expect(identity.publicKey).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should use spec-compliant did:key fragment (not #keys-1)", async () => {
|
|
18
|
+
const identity = await generateIdentity(crypto);
|
|
19
|
+
const fragment = identity.kid.split("#")[1];
|
|
20
|
+
|
|
21
|
+
expect(fragment).not.toBe("keys-1");
|
|
22
|
+
expect(fragment).toMatch(/^z6Mk/);
|
|
23
|
+
expect(identity.kid).toBe(`${identity.did}#${identity.did.replace("did:key:", "")}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should generate unique identities each call", async () => {
|
|
27
|
+
const a = await generateIdentity(crypto);
|
|
28
|
+
const b = await generateIdentity(crypto);
|
|
29
|
+
|
|
30
|
+
expect(a.did).not.toBe(b.did);
|
|
31
|
+
expect(a.privateKey).not.toBe(b.privateKey);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("withMCPI", () => {
|
|
36
|
+
it("should register _mcpi tool on server by default", async () => {
|
|
37
|
+
const registerTool = vi.fn();
|
|
38
|
+
const server = {
|
|
39
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
registerTool,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
await withMCPI(server, { crypto });
|
|
44
|
+
|
|
45
|
+
expect(registerTool).toHaveBeenCalledWith(
|
|
46
|
+
"_mcpi",
|
|
47
|
+
expect.objectContaining({ description: expect.any(String) }),
|
|
48
|
+
expect.any(Function),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should not register tool when handshakeExposure is 'none'", async () => {
|
|
53
|
+
const registerTool = vi.fn();
|
|
54
|
+
const server = {
|
|
55
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
56
|
+
registerTool,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await withMCPI(server, { crypto, handshakeExposure: "none" });
|
|
60
|
+
|
|
61
|
+
expect(registerTool).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should patch server.connect to wrap transport", async () => {
|
|
65
|
+
const originalConnect = vi.fn().mockResolvedValue(undefined);
|
|
66
|
+
const server = {
|
|
67
|
+
connect: originalConnect,
|
|
68
|
+
registerTool: vi.fn(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await withMCPI(server, { crypto });
|
|
72
|
+
|
|
73
|
+
// server.connect should now be patched
|
|
74
|
+
expect(server.connect).not.toBe(originalConnect);
|
|
75
|
+
|
|
76
|
+
// Call the patched connect
|
|
77
|
+
const mockTransport = {
|
|
78
|
+
start: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
send: vi.fn().mockResolvedValue(undefined),
|
|
80
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
81
|
+
};
|
|
82
|
+
await server.connect(mockTransport);
|
|
83
|
+
|
|
84
|
+
// Original connect should have been called with wrapped transport
|
|
85
|
+
expect(originalConnect).toHaveBeenCalledTimes(1);
|
|
86
|
+
// The argument should be the wrapped transport (not the original)
|
|
87
|
+
const wrappedTransport = originalConnect.mock.calls[0][0];
|
|
88
|
+
expect(wrappedTransport).not.toBe(mockTransport);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should not patch connect when proofAllTools is false", async () => {
|
|
92
|
+
const originalConnect = vi.fn().mockResolvedValue(undefined);
|
|
93
|
+
const server = {
|
|
94
|
+
connect: originalConnect,
|
|
95
|
+
registerTool: vi.fn(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await withMCPI(server, { crypto, proofAllTools: false });
|
|
99
|
+
|
|
100
|
+
// connect should NOT be patched
|
|
101
|
+
expect(server.connect).toBe(originalConnect);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return MCPIMiddleware instance", async () => {
|
|
105
|
+
const server = {
|
|
106
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
107
|
+
registerTool: vi.fn(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const mcpi = await withMCPI(server, { crypto });
|
|
111
|
+
|
|
112
|
+
expect(mcpi).toBeDefined();
|
|
113
|
+
expect(mcpi.wrapWithProof).toBeInstanceOf(Function);
|
|
114
|
+
expect(mcpi.wrapWithDelegation).toBeInstanceOf(Function);
|
|
115
|
+
expect(mcpi.handleMCPI).toBeInstanceOf(Function);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
createMCPIMiddleware,
|
|
4
4
|
type MCPIDelegationConfig,
|
|
@@ -24,7 +24,7 @@ async function createTestMiddleware(options?: {
|
|
|
24
24
|
const crypto = new NodeCryptoProvider();
|
|
25
25
|
const keyPair = await crypto.generateKeyPair();
|
|
26
26
|
const did = generateDidKeyFromBase64(keyPair.publicKey);
|
|
27
|
-
const kid = `${did}
|
|
27
|
+
const kid = `${did}#${did.replace('did:key:', '')}`;
|
|
28
28
|
|
|
29
29
|
const middleware = createMCPIMiddleware(
|
|
30
30
|
{
|
|
@@ -36,14 +36,14 @@ async function createTestMiddleware(options?: {
|
|
|
36
36
|
crypto,
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
return { middleware, did };
|
|
39
|
+
return { middleware, did, crypto };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async function createDelegationIssuer(options?: { did?: string; kid?: string }) {
|
|
43
43
|
const crypto = new NodeCryptoProvider();
|
|
44
44
|
const keyPair = await crypto.generateKeyPair();
|
|
45
45
|
const did = options?.did ?? generateDidKeyFromBase64(keyPair.publicKey);
|
|
46
|
-
const kid = options?.kid ?? `${did}
|
|
46
|
+
const kid = options?.kid ?? `${did}#${did.replace('did:key:', '')}`;
|
|
47
47
|
|
|
48
48
|
const signingFn = async (
|
|
49
49
|
canonicalVC: string,
|
|
@@ -126,7 +126,95 @@ describe('createMCPIMiddleware', () => {
|
|
|
126
126
|
expect(result.isError).toBe(true);
|
|
127
127
|
const parsed = JSON.parse(result.content[0].text);
|
|
128
128
|
expect(parsed.success).toBe(false);
|
|
129
|
-
expect(parsed.error.code).toBe('
|
|
129
|
+
expect(parsed.error.code).toBe('handshake_failed');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('_mcpi unified tool', () => {
|
|
134
|
+
it('should expose mcpiTool with name "_mcpi"', async () => {
|
|
135
|
+
const { middleware: mcpi } = await createTestMiddleware();
|
|
136
|
+
expect(mcpi.mcpiTool.name).toBe('_mcpi');
|
|
137
|
+
expect(mcpi.mcpiTool.inputSchema.properties?.action).toBeDefined();
|
|
138
|
+
expect(
|
|
139
|
+
(mcpi.mcpiTool.inputSchema.properties?.action as { enum?: string[] })?.enum,
|
|
140
|
+
).toContain('handshake');
|
|
141
|
+
expect(
|
|
142
|
+
(mcpi.mcpiTool.inputSchema.properties?.action as { enum?: string[] })?.enum,
|
|
143
|
+
).toContain('identity');
|
|
144
|
+
expect(mcpi.mcpiTool.inputSchema.required).toEqual(['action']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should still expose handshakeTool as deprecated alias', async () => {
|
|
148
|
+
const { middleware: mcpi } = await createTestMiddleware();
|
|
149
|
+
expect(mcpi.handshakeTool).toBeDefined();
|
|
150
|
+
expect(mcpi.handshakeTool.name).toBe('_mcpi_handshake');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should dispatch action: "handshake" to handleHandshake', async () => {
|
|
154
|
+
const { middleware: mcpi, did } = await createTestMiddleware();
|
|
155
|
+
const result = await mcpi.handleMCPI({
|
|
156
|
+
action: 'handshake',
|
|
157
|
+
nonce: 'test-nonce',
|
|
158
|
+
audience: did,
|
|
159
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.isError).toBeUndefined();
|
|
163
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
164
|
+
expect(parsed.success).toBe(true);
|
|
165
|
+
expect(parsed.sessionId).toMatch(/^mcpi_/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should dispatch action: "identity" and return server metadata', async () => {
|
|
169
|
+
const { middleware: mcpi, did } = await createTestMiddleware();
|
|
170
|
+
const result = await mcpi.handleMCPI({ action: 'identity' });
|
|
171
|
+
|
|
172
|
+
expect(result.isError).toBeUndefined();
|
|
173
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
174
|
+
expect(parsed.did).toBe(did);
|
|
175
|
+
expect(parsed.kid).toMatch(/#z[\w]+$/);
|
|
176
|
+
expect(parsed.capabilities).toContain('handshake');
|
|
177
|
+
expect(parsed.capabilities).toContain('signing');
|
|
178
|
+
expect(parsed.capabilities).toContain('verification');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return error for unknown action', async () => {
|
|
182
|
+
const { middleware: mcpi } = await createTestMiddleware();
|
|
183
|
+
const result = await mcpi.handleMCPI({ action: 'does_not_exist' });
|
|
184
|
+
|
|
185
|
+
expect(result.isError).toBe(true);
|
|
186
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
187
|
+
expect(parsed.error.code).toBe('invalid_request');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should return error when action is missing', async () => {
|
|
191
|
+
const { middleware: mcpi } = await createTestMiddleware();
|
|
192
|
+
const result = await mcpi.handleMCPI({});
|
|
193
|
+
|
|
194
|
+
expect(result.isError).toBe(true);
|
|
195
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
196
|
+
expect(parsed.error.code).toBe('invalid_request');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return "not implemented" for action: "reputation"', async () => {
|
|
200
|
+
const { middleware: mcpi } = await createTestMiddleware();
|
|
201
|
+
const result = await mcpi.handleMCPI({ action: 'reputation' });
|
|
202
|
+
|
|
203
|
+
expect(result.isError).toBe(true);
|
|
204
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
205
|
+
expect(parsed.error.code).toBe('runtime_error');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should still support handleHandshake() directly (backward compat)', async () => {
|
|
209
|
+
const { middleware: mcpi, did } = await createTestMiddleware();
|
|
210
|
+
const result = await mcpi.handleHandshake({
|
|
211
|
+
nonce: 'legacy-nonce',
|
|
212
|
+
audience: did,
|
|
213
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
217
|
+
expect(parsed.success).toBe(true);
|
|
130
218
|
});
|
|
131
219
|
});
|
|
132
220
|
|
|
@@ -195,6 +283,36 @@ describe('createMCPIMiddleware', () => {
|
|
|
195
283
|
expect(result.content[0].text).toBe('Hello!');
|
|
196
284
|
expect(result._meta).toBeUndefined();
|
|
197
285
|
});
|
|
286
|
+
|
|
287
|
+
it('should surface proofError in _meta when proof generation fails', async () => {
|
|
288
|
+
const { middleware: mcpi, did, crypto } = await createTestMiddleware();
|
|
289
|
+
|
|
290
|
+
// Handshake first
|
|
291
|
+
const hs = await mcpi.handleHandshake({
|
|
292
|
+
nonce: 'test-nonce-proof-fail',
|
|
293
|
+
audience: did,
|
|
294
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
295
|
+
});
|
|
296
|
+
const sessionId = JSON.parse(hs.content[0].text).sessionId;
|
|
297
|
+
|
|
298
|
+
// Make crypto.hash throw to break proof generation after handshake succeeds
|
|
299
|
+
vi.spyOn(crypto, 'hash').mockRejectedValue(new Error('HSM unavailable'));
|
|
300
|
+
|
|
301
|
+
const handler = mcpi.wrapWithProof('greet', async () => ({
|
|
302
|
+
content: [{ type: 'text', text: 'Hello!' }],
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
const result = await handler({}, sessionId);
|
|
306
|
+
|
|
307
|
+
// Tool result still returned
|
|
308
|
+
expect(result.content[0].text).toBe('Hello!');
|
|
309
|
+
expect(result.isError).toBeUndefined();
|
|
310
|
+
|
|
311
|
+
// But _meta signals the proof failure
|
|
312
|
+
expect(result._meta).toBeDefined();
|
|
313
|
+
expect(result._meta!.proofError).toBeDefined();
|
|
314
|
+
expect(result._meta!.proof).toBeUndefined();
|
|
315
|
+
});
|
|
198
316
|
});
|
|
199
317
|
|
|
200
318
|
describe('wrapWithDelegation', () => {
|
|
@@ -232,7 +350,7 @@ describe('createMCPIMiddleware', () => {
|
|
|
232
350
|
|
|
233
351
|
expect(result.isError).toBe(true);
|
|
234
352
|
const parsed = JSON.parse(result.content[0].text);
|
|
235
|
-
expect(parsed.error).toBe('
|
|
353
|
+
expect(parsed.error).toBe('insufficient_scope');
|
|
236
354
|
});
|
|
237
355
|
|
|
238
356
|
it('should accept and call handler when VC has correct scope and valid signature', async () => {
|