@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
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}#keys-1`;
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}#keys-1`;
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('MCPI_INVALID_HANDSHAKE');
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('delegation_scope_missing');
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 () => {
@@ -23,3 +23,9 @@ export {
23
23
  generateIdentity,
24
24
  type WithMCPIOptions,
25
25
  } from './with-mcpi-server.js';
26
+
27
+ export {
28
+ createMCPITransport,
29
+ type Transport,
30
+ type JSONRPCMessage,
31
+ } from './mcpi-transport.js';