@mcp-i/core 0.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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +390 -0
  3. package/dist/auth/handshake.d.ts +104 -0
  4. package/dist/auth/handshake.d.ts.map +1 -0
  5. package/dist/auth/handshake.js +230 -0
  6. package/dist/auth/handshake.js.map +1 -0
  7. package/dist/auth/index.d.ts +3 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +2 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/types.d.ts +31 -0
  12. package/dist/auth/types.d.ts.map +1 -0
  13. package/dist/auth/types.js +7 -0
  14. package/dist/auth/types.js.map +1 -0
  15. package/dist/delegation/audience-validator.d.ts +9 -0
  16. package/dist/delegation/audience-validator.d.ts.map +1 -0
  17. package/dist/delegation/audience-validator.js +17 -0
  18. package/dist/delegation/audience-validator.js.map +1 -0
  19. package/dist/delegation/bitstring.d.ts +37 -0
  20. package/dist/delegation/bitstring.d.ts.map +1 -0
  21. package/dist/delegation/bitstring.js +117 -0
  22. package/dist/delegation/bitstring.js.map +1 -0
  23. package/dist/delegation/cascading-revocation.d.ts +45 -0
  24. package/dist/delegation/cascading-revocation.d.ts.map +1 -0
  25. package/dist/delegation/cascading-revocation.js +148 -0
  26. package/dist/delegation/cascading-revocation.js.map +1 -0
  27. package/dist/delegation/delegation-graph.d.ts +49 -0
  28. package/dist/delegation/delegation-graph.d.ts.map +1 -0
  29. package/dist/delegation/delegation-graph.js +99 -0
  30. package/dist/delegation/delegation-graph.js.map +1 -0
  31. package/dist/delegation/did-key-resolver.d.ts +64 -0
  32. package/dist/delegation/did-key-resolver.d.ts.map +1 -0
  33. package/dist/delegation/did-key-resolver.js +154 -0
  34. package/dist/delegation/did-key-resolver.js.map +1 -0
  35. package/dist/delegation/did-web-resolver.d.ts +83 -0
  36. package/dist/delegation/did-web-resolver.d.ts.map +1 -0
  37. package/dist/delegation/did-web-resolver.js +218 -0
  38. package/dist/delegation/did-web-resolver.js.map +1 -0
  39. package/dist/delegation/index.d.ts +21 -0
  40. package/dist/delegation/index.d.ts.map +1 -0
  41. package/dist/delegation/index.js +21 -0
  42. package/dist/delegation/index.js.map +1 -0
  43. package/dist/delegation/outbound-headers.d.ts +81 -0
  44. package/dist/delegation/outbound-headers.d.ts.map +1 -0
  45. package/dist/delegation/outbound-headers.js +139 -0
  46. package/dist/delegation/outbound-headers.js.map +1 -0
  47. package/dist/delegation/outbound-proof.d.ts +43 -0
  48. package/dist/delegation/outbound-proof.d.ts.map +1 -0
  49. package/dist/delegation/outbound-proof.js +52 -0
  50. package/dist/delegation/outbound-proof.js.map +1 -0
  51. package/dist/delegation/statuslist-manager.d.ts +44 -0
  52. package/dist/delegation/statuslist-manager.d.ts.map +1 -0
  53. package/dist/delegation/statuslist-manager.js +126 -0
  54. package/dist/delegation/statuslist-manager.js.map +1 -0
  55. package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
  56. package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
  57. package/dist/delegation/storage/memory-graph-storage.js +145 -0
  58. package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
  59. package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
  60. package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
  61. package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
  62. package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
  63. package/dist/delegation/utils.d.ts +49 -0
  64. package/dist/delegation/utils.d.ts.map +1 -0
  65. package/dist/delegation/utils.js +131 -0
  66. package/dist/delegation/utils.js.map +1 -0
  67. package/dist/delegation/vc-issuer.d.ts +56 -0
  68. package/dist/delegation/vc-issuer.d.ts.map +1 -0
  69. package/dist/delegation/vc-issuer.js +80 -0
  70. package/dist/delegation/vc-issuer.js.map +1 -0
  71. package/dist/delegation/vc-verifier.d.ts +112 -0
  72. package/dist/delegation/vc-verifier.d.ts.map +1 -0
  73. package/dist/delegation/vc-verifier.js +280 -0
  74. package/dist/delegation/vc-verifier.js.map +1 -0
  75. package/dist/index.d.ts +45 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +53 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/logging/index.d.ts +2 -0
  80. package/dist/logging/index.d.ts.map +1 -0
  81. package/dist/logging/index.js +2 -0
  82. package/dist/logging/index.js.map +1 -0
  83. package/dist/logging/logger.d.ts +23 -0
  84. package/dist/logging/logger.d.ts.map +1 -0
  85. package/dist/logging/logger.js +82 -0
  86. package/dist/logging/logger.js.map +1 -0
  87. package/dist/middleware/index.d.ts +7 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +7 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/with-mcpi.d.ts +152 -0
  92. package/dist/middleware/with-mcpi.d.ts.map +1 -0
  93. package/dist/middleware/with-mcpi.js +472 -0
  94. package/dist/middleware/with-mcpi.js.map +1 -0
  95. package/dist/proof/errors.d.ts +49 -0
  96. package/dist/proof/errors.d.ts.map +1 -0
  97. package/dist/proof/errors.js +61 -0
  98. package/dist/proof/errors.js.map +1 -0
  99. package/dist/proof/generator.d.ts +65 -0
  100. package/dist/proof/generator.d.ts.map +1 -0
  101. package/dist/proof/generator.js +163 -0
  102. package/dist/proof/generator.js.map +1 -0
  103. package/dist/proof/index.d.ts +4 -0
  104. package/dist/proof/index.d.ts.map +1 -0
  105. package/dist/proof/index.js +4 -0
  106. package/dist/proof/index.js.map +1 -0
  107. package/dist/proof/verifier.d.ts +108 -0
  108. package/dist/proof/verifier.d.ts.map +1 -0
  109. package/dist/proof/verifier.js +299 -0
  110. package/dist/proof/verifier.js.map +1 -0
  111. package/dist/providers/base.d.ts +64 -0
  112. package/dist/providers/base.d.ts.map +1 -0
  113. package/dist/providers/base.js +19 -0
  114. package/dist/providers/base.js.map +1 -0
  115. package/dist/providers/index.d.ts +3 -0
  116. package/dist/providers/index.d.ts.map +1 -0
  117. package/dist/providers/index.js +3 -0
  118. package/dist/providers/index.js.map +1 -0
  119. package/dist/providers/memory.d.ts +33 -0
  120. package/dist/providers/memory.d.ts.map +1 -0
  121. package/dist/providers/memory.js +102 -0
  122. package/dist/providers/memory.js.map +1 -0
  123. package/dist/session/index.d.ts +2 -0
  124. package/dist/session/index.d.ts.map +1 -0
  125. package/dist/session/index.js +2 -0
  126. package/dist/session/index.js.map +1 -0
  127. package/dist/session/manager.d.ts +77 -0
  128. package/dist/session/manager.d.ts.map +1 -0
  129. package/dist/session/manager.js +251 -0
  130. package/dist/session/manager.js.map +1 -0
  131. package/dist/types/protocol.d.ts +320 -0
  132. package/dist/types/protocol.d.ts.map +1 -0
  133. package/dist/types/protocol.js +229 -0
  134. package/dist/types/protocol.js.map +1 -0
  135. package/dist/utils/base58.d.ts +31 -0
  136. package/dist/utils/base58.d.ts.map +1 -0
  137. package/dist/utils/base58.js +104 -0
  138. package/dist/utils/base58.js.map +1 -0
  139. package/dist/utils/base64.d.ts +13 -0
  140. package/dist/utils/base64.d.ts.map +1 -0
  141. package/dist/utils/base64.js +99 -0
  142. package/dist/utils/base64.js.map +1 -0
  143. package/dist/utils/crypto-service.d.ts +37 -0
  144. package/dist/utils/crypto-service.d.ts.map +1 -0
  145. package/dist/utils/crypto-service.js +153 -0
  146. package/dist/utils/crypto-service.js.map +1 -0
  147. package/dist/utils/did-helpers.d.ts +156 -0
  148. package/dist/utils/did-helpers.d.ts.map +1 -0
  149. package/dist/utils/did-helpers.js +193 -0
  150. package/dist/utils/did-helpers.js.map +1 -0
  151. package/dist/utils/ed25519-constants.d.ts +18 -0
  152. package/dist/utils/ed25519-constants.d.ts.map +1 -0
  153. package/dist/utils/ed25519-constants.js +21 -0
  154. package/dist/utils/ed25519-constants.js.map +1 -0
  155. package/dist/utils/index.d.ts +5 -0
  156. package/dist/utils/index.d.ts.map +1 -0
  157. package/dist/utils/index.js +5 -0
  158. package/dist/utils/index.js.map +1 -0
  159. package/package.json +105 -0
  160. package/src/__tests__/integration/full-flow.test.ts +362 -0
  161. package/src/__tests__/providers/base.test.ts +173 -0
  162. package/src/__tests__/providers/memory.test.ts +332 -0
  163. package/src/__tests__/utils/mock-providers.ts +319 -0
  164. package/src/__tests__/utils/node-crypto-provider.ts +93 -0
  165. package/src/auth/handshake.ts +411 -0
  166. package/src/auth/index.ts +11 -0
  167. package/src/auth/types.ts +40 -0
  168. package/src/delegation/__tests__/audience-validator.test.ts +110 -0
  169. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  170. package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
  171. package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
  172. package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
  173. package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
  174. package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
  175. package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
  176. package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
  177. package/src/delegation/__tests__/utils.test.ts +185 -0
  178. package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
  179. package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
  180. package/src/delegation/audience-validator.ts +24 -0
  181. package/src/delegation/bitstring.ts +160 -0
  182. package/src/delegation/cascading-revocation.ts +224 -0
  183. package/src/delegation/delegation-graph.ts +143 -0
  184. package/src/delegation/did-key-resolver.ts +181 -0
  185. package/src/delegation/did-web-resolver.ts +270 -0
  186. package/src/delegation/index.ts +33 -0
  187. package/src/delegation/outbound-headers.ts +193 -0
  188. package/src/delegation/outbound-proof.ts +90 -0
  189. package/src/delegation/statuslist-manager.ts +219 -0
  190. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  191. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  192. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  193. package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
  194. package/src/delegation/utils.ts +189 -0
  195. package/src/delegation/vc-issuer.ts +137 -0
  196. package/src/delegation/vc-verifier.ts +440 -0
  197. package/src/index.ts +264 -0
  198. package/src/logging/__tests__/logger.test.ts +366 -0
  199. package/src/logging/index.ts +6 -0
  200. package/src/logging/logger.ts +91 -0
  201. package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
  202. package/src/middleware/index.ts +16 -0
  203. package/src/middleware/with-mcpi.ts +766 -0
  204. package/src/proof/__tests__/proof-generator.test.ts +483 -0
  205. package/src/proof/__tests__/verifier.test.ts +488 -0
  206. package/src/proof/errors.ts +75 -0
  207. package/src/proof/generator.ts +255 -0
  208. package/src/proof/index.ts +22 -0
  209. package/src/proof/verifier.ts +449 -0
  210. package/src/providers/base.ts +68 -0
  211. package/src/providers/index.ts +15 -0
  212. package/src/providers/memory.ts +130 -0
  213. package/src/session/__tests__/session-manager.test.ts +342 -0
  214. package/src/session/index.ts +7 -0
  215. package/src/session/manager.ts +332 -0
  216. package/src/types/protocol.ts +596 -0
  217. package/src/utils/__tests__/base58.test.ts +281 -0
  218. package/src/utils/__tests__/base64.test.ts +239 -0
  219. package/src/utils/__tests__/crypto-service.test.ts +530 -0
  220. package/src/utils/__tests__/did-helpers.test.ts +156 -0
  221. package/src/utils/base58.ts +115 -0
  222. package/src/utils/base64.ts +116 -0
  223. package/src/utils/crypto-service.ts +209 -0
  224. package/src/utils/did-helpers.ts +210 -0
  225. package/src/utils/ed25519-constants.ts +23 -0
  226. package/src/utils/index.ts +9 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Transport-aware Logger for MCP-I
3
+ *
4
+ * Provides a lightweight, dependency-free logging interface that:
5
+ * - Maps log levels correctly for Cloudflare Workers
6
+ * - Routes all logs to stderr for stdio transport (so stdout remains protocol-only)
7
+ * - Supports runtime configuration of log level and transport mode
8
+ */
9
+
10
+ export type Level = 'debug' | 'info' | 'warn' | 'error';
11
+
12
+ const SEVERITY: Record<Level, number> = {
13
+ debug: 10,
14
+ info: 20,
15
+ warn: 30,
16
+ error: 40,
17
+ };
18
+
19
+ export interface Logger {
20
+ configure: (opts?: { level?: Level; transport?: string; forceStderr?: boolean }) => void;
21
+ debug: (...args: unknown[]) => void;
22
+ info: (...args: unknown[]) => void;
23
+ warn: (...args: unknown[]) => void;
24
+ error: (...args: unknown[]) => void;
25
+ }
26
+
27
+ export function createDefaultConsoleLogger(): Logger {
28
+ let level: Level = 'info';
29
+ let forceStderr = false;
30
+
31
+ function shouldLog(l: Level): boolean {
32
+ return SEVERITY[l] >= SEVERITY[level];
33
+ }
34
+
35
+ function write(l: Level, ...args: unknown[]): void {
36
+ if (!shouldLog(l)) return;
37
+
38
+ if (forceStderr) {
39
+ console.error(...args);
40
+ return;
41
+ }
42
+
43
+ switch (l) {
44
+ case 'debug':
45
+ if (typeof console.debug === 'function') {
46
+ console.debug(...args);
47
+ } else {
48
+ console.log(...args);
49
+ }
50
+ break;
51
+ case 'info':
52
+ if (typeof console.info === 'function') {
53
+ console.info(...args);
54
+ } else {
55
+ console.log(...args);
56
+ }
57
+ break;
58
+ case 'warn':
59
+ if (typeof console.warn === 'function') {
60
+ console.warn(...args);
61
+ } else {
62
+ console.error(...args);
63
+ }
64
+ break;
65
+ case 'error':
66
+ console.error(...args);
67
+ break;
68
+ }
69
+ }
70
+
71
+ return {
72
+ configure(opts = {}) {
73
+ if (opts.level) level = opts.level;
74
+ if (opts.forceStderr === true) {
75
+ forceStderr = true;
76
+ } else if (opts.forceStderr === false) {
77
+ forceStderr = false;
78
+ } else if (opts.transport === 'stdio') {
79
+ forceStderr = true;
80
+ } else if (opts.transport === 'sse' || opts.transport === 'http') {
81
+ forceStderr = false;
82
+ }
83
+ },
84
+ debug: (...a) => write('debug', ...a),
85
+ info: (...a) => write('info', ...a),
86
+ warn: (...a) => write('warn', ...a),
87
+ error: (...a) => write('error', ...a),
88
+ };
89
+ }
90
+
91
+ export const logger = createDefaultConsoleLogger();
@@ -0,0 +1,504 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createMCPIMiddleware,
4
+ type MCPIDelegationConfig,
5
+ } from '../with-mcpi.js';
6
+ import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
7
+ import { MockFetchProvider } from '../../__tests__/utils/mock-providers.js';
8
+ import { generateDidKeyFromBase64 } from '../../utils/did-helpers.js';
9
+ import { DelegationCredentialIssuer } from '../../delegation/vc-issuer.js';
10
+ import type {
11
+ CredentialStatus,
12
+ DelegationCredential,
13
+ Proof,
14
+ } from '../../types/protocol.js';
15
+ import {
16
+ base64ToBytes,
17
+ base64urlEncodeFromBytes,
18
+ } from '../../utils/base64.js';
19
+
20
+ async function createTestMiddleware(options?: {
21
+ autoSession?: boolean;
22
+ delegation?: MCPIDelegationConfig;
23
+ }) {
24
+ const crypto = new NodeCryptoProvider();
25
+ const keyPair = await crypto.generateKeyPair();
26
+ const did = generateDidKeyFromBase64(keyPair.publicKey);
27
+ const kid = `${did}#keys-1`;
28
+
29
+ const middleware = createMCPIMiddleware(
30
+ {
31
+ identity: { did, kid, privateKey: keyPair.privateKey, publicKey: keyPair.publicKey },
32
+ session: { sessionTtlMinutes: 60 },
33
+ delegation: options?.delegation,
34
+ autoSession: options?.autoSession,
35
+ },
36
+ crypto,
37
+ );
38
+
39
+ return { middleware, did };
40
+ }
41
+
42
+ async function createDelegationIssuer(options?: { did?: string; kid?: string }) {
43
+ const crypto = new NodeCryptoProvider();
44
+ const keyPair = await crypto.generateKeyPair();
45
+ const did = options?.did ?? generateDidKeyFromBase64(keyPair.publicKey);
46
+ const kid = options?.kid ?? `${did}#keys-1`;
47
+
48
+ const signingFn = async (
49
+ canonicalVC: string,
50
+ _issuerDid: string,
51
+ kidArg: string,
52
+ ): Promise<Proof> => {
53
+ const data = new TextEncoder().encode(canonicalVC);
54
+ const sigBytes = await crypto.sign(data, keyPair.privateKey);
55
+ const proofValue = base64urlEncodeFromBytes(sigBytes);
56
+ return {
57
+ type: 'Ed25519Signature2020',
58
+ created: new Date().toISOString(),
59
+ verificationMethod: kidArg,
60
+ proofPurpose: 'assertionMethod',
61
+ proofValue,
62
+ };
63
+ };
64
+
65
+ const issuer = new DelegationCredentialIssuer(
66
+ {
67
+ getDid: () => did,
68
+ getKeyId: () => kid,
69
+ getPrivateKey: () => keyPair.privateKey,
70
+ },
71
+ signingFn,
72
+ );
73
+
74
+ return { crypto, keyPair, did, kid, issuer };
75
+ }
76
+
77
+ async function issueDelegationVC(options?: {
78
+ issuer?: Awaited<ReturnType<typeof createDelegationIssuer>>;
79
+ scopes?: string[];
80
+ audience?: string | string[];
81
+ parentId?: string;
82
+ credentialStatus?: CredentialStatus;
83
+ subjectDid?: string;
84
+ }): Promise<DelegationCredential> {
85
+ const issuerIdentity = options?.issuer ?? await createDelegationIssuer();
86
+
87
+ return issuerIdentity.issuer.createAndIssueDelegation(
88
+ {
89
+ id: `test-delegation-${Date.now()}-${Math.random().toString(16).slice(2)}`,
90
+ issuerDid: issuerIdentity.did,
91
+ subjectDid: options?.subjectDid ?? issuerIdentity.did,
92
+ parentId: options?.parentId,
93
+ constraints: {
94
+ scopes: options?.scopes ?? [],
95
+ ...(options?.audience !== undefined && { audience: options.audience }),
96
+ notAfter: Math.floor(Date.now() / 1000) + 3600,
97
+ },
98
+ },
99
+ ...(options?.credentialStatus
100
+ ? [{ credentialStatus: options.credentialStatus }]
101
+ : []),
102
+ );
103
+ }
104
+
105
+ describe('createMCPIMiddleware', () => {
106
+ describe('handleHandshake', () => {
107
+ it('should establish a session with valid handshake', async () => {
108
+ const { middleware: mcpi, did } = await createTestMiddleware();
109
+ const result = await mcpi.handleHandshake({
110
+ nonce: 'test-nonce',
111
+ audience: did,
112
+ timestamp: Math.floor(Date.now() / 1000),
113
+ });
114
+
115
+ expect(result.isError).toBeUndefined();
116
+ const parsed = JSON.parse(result.content[0].text);
117
+ expect(parsed.success).toBe(true);
118
+ expect(parsed.sessionId).toMatch(/^mcpi_/);
119
+ expect(parsed.serverDid).toMatch(/^did:key:/);
120
+ });
121
+
122
+ it('should reject invalid handshake', async () => {
123
+ const { middleware: mcpi } = await createTestMiddleware();
124
+ const result = await mcpi.handleHandshake({ nonce: 'test' });
125
+
126
+ expect(result.isError).toBe(true);
127
+ const parsed = JSON.parse(result.content[0].text);
128
+ expect(parsed.success).toBe(false);
129
+ expect(parsed.error.code).toBe('MCPI_INVALID_HANDSHAKE');
130
+ });
131
+ });
132
+
133
+ describe('wrapWithProof', () => {
134
+ it('should attach proof in _meta after handshake', async () => {
135
+ const { middleware: mcpi, did } = await createTestMiddleware();
136
+
137
+ // Handshake first
138
+ const hs = await mcpi.handleHandshake({
139
+ nonce: 'test-nonce',
140
+ audience: did,
141
+ timestamp: Math.floor(Date.now() / 1000),
142
+ });
143
+ const sessionId = JSON.parse(hs.content[0].text).sessionId;
144
+
145
+ // Call wrapped tool
146
+ const handler = mcpi.wrapWithProof('greet', async (args) => ({
147
+ content: [{ type: 'text', text: `Hello, ${args['name']}!` }],
148
+ }));
149
+
150
+ const result = await handler({ name: 'DIF' }, sessionId);
151
+
152
+ // Tool result in content (single block)
153
+ expect(result.content).toHaveLength(1);
154
+ expect(result.content[0].text).toBe('Hello, DIF!');
155
+
156
+ // Proof in _meta, not in content
157
+ expect(result._meta).toBeDefined();
158
+ expect(result._meta!.proof).toBeDefined();
159
+ const proof = result._meta!.proof as { jws: string; meta: Record<string, unknown> };
160
+ expect(proof.jws).toBeDefined();
161
+ expect(proof.meta.did).toMatch(/^did:key:/);
162
+ expect(proof.meta.sessionId).toBe(sessionId);
163
+ expect(proof.meta.requestHash).toMatch(/^sha256:[a-f0-9]{64}$/);
164
+ expect(proof.meta.responseHash).toMatch(/^sha256:[a-f0-9]{64}$/);
165
+ });
166
+
167
+ it('should not attach proof when result is an error', async () => {
168
+ const { middleware: mcpi, did } = await createTestMiddleware();
169
+
170
+ const hs = await mcpi.handleHandshake({
171
+ nonce: 'test-nonce',
172
+ audience: did,
173
+ timestamp: Math.floor(Date.now() / 1000),
174
+ });
175
+ const sessionId = JSON.parse(hs.content[0].text).sessionId;
176
+
177
+ const handler = mcpi.wrapWithProof('fail-tool', async () => ({
178
+ content: [{ type: 'text', text: 'error' }],
179
+ isError: true,
180
+ }));
181
+
182
+ const result = await handler({}, sessionId);
183
+ expect(result.isError).toBe(true);
184
+ expect(result._meta).toBeUndefined();
185
+ });
186
+
187
+ it('should return result without proof when no session exists and autoSession is off', async () => {
188
+ const { middleware: mcpi } = await createTestMiddleware({ autoSession: false });
189
+
190
+ const handler = mcpi.wrapWithProof('greet', async () => ({
191
+ content: [{ type: 'text', text: 'Hello!' }],
192
+ }));
193
+
194
+ const result = await handler({});
195
+ expect(result.content[0].text).toBe('Hello!');
196
+ expect(result._meta).toBeUndefined();
197
+ });
198
+ });
199
+
200
+ describe('wrapWithDelegation', () => {
201
+ it('should return needs_authorization when no _mcpi_delegation arg is provided', async () => {
202
+ const { middleware: mcpi } = await createTestMiddleware();
203
+
204
+ const handler = mcpi.wrapWithDelegation(
205
+ 'my-tool',
206
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
207
+ async () => ({ content: [{ type: 'text', text: 'should not reach' }] }),
208
+ );
209
+
210
+ const result = await handler({ name: 'world' });
211
+
212
+ expect(result.isError).toBeUndefined();
213
+ const parsed = JSON.parse(result.content[0].text);
214
+ expect(parsed.error).toBe('needs_authorization');
215
+ expect(parsed.authorizationUrl).toBe('https://example.com/consent');
216
+ expect(parsed.scopes).toContain('test:scope');
217
+ expect(typeof parsed.resumeToken).toBe('string');
218
+ expect(typeof parsed.expiresAt).toBe('number');
219
+ });
220
+
221
+ it('should reject when VC has wrong scope', async () => {
222
+ const { middleware: mcpi } = await createTestMiddleware();
223
+ const vc = await issueDelegationVC({ scopes: ['wrong:scope'] });
224
+
225
+ const handler = mcpi.wrapWithDelegation(
226
+ 'my-tool',
227
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
228
+ async () => ({ content: [{ type: 'text', text: 'should not reach' }] }),
229
+ );
230
+
231
+ const result = await handler({ _mcpi_delegation: vc });
232
+
233
+ expect(result.isError).toBe(true);
234
+ const parsed = JSON.parse(result.content[0].text);
235
+ expect(parsed.error).toBe('delegation_scope_missing');
236
+ });
237
+
238
+ it('should accept and call handler when VC has correct scope and valid signature', async () => {
239
+ const { middleware: mcpi } = await createTestMiddleware();
240
+ const vc = await issueDelegationVC({ scopes: ['test:scope', 'other:scope'] });
241
+
242
+ const handler = mcpi.wrapWithDelegation(
243
+ 'my-tool',
244
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
245
+ async (args) => ({
246
+ content: [{ type: 'text', text: `Called: ${JSON.stringify(args)}` }],
247
+ }),
248
+ );
249
+
250
+ const result = await handler({ _mcpi_delegation: vc, name: 'DIF' });
251
+
252
+ expect(result.isError).toBeUndefined();
253
+ const parsed = JSON.parse(result.content[0].text.replace('Called: ', ''));
254
+ // _mcpi_delegation should be stripped from args
255
+ expect(parsed['_mcpi_delegation']).toBeUndefined();
256
+ expect(parsed['name']).toBe('DIF');
257
+ });
258
+
259
+ it('should reject credentials with credentialStatus when no status list resolver is configured', async () => {
260
+ const { middleware: mcpi } = await createTestMiddleware();
261
+ const vc = await issueDelegationVC({
262
+ scopes: ['test:scope'],
263
+ credentialStatus: {
264
+ id: 'https://status.example.com/revocation/v1#0',
265
+ type: 'StatusList2021Entry',
266
+ statusPurpose: 'revocation',
267
+ statusListIndex: '0',
268
+ statusListCredential: 'https://status.example.com/revocation/v1',
269
+ },
270
+ });
271
+
272
+ const handler = mcpi.wrapWithDelegation(
273
+ 'my-tool',
274
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
275
+ async () => ({ content: [{ type: 'text', text: 'should not reach' }] }),
276
+ );
277
+
278
+ const result = await handler({ _mcpi_delegation: vc });
279
+
280
+ expect(result.isError).toBe(true);
281
+ const parsed = JSON.parse(result.content[0].text);
282
+ expect(parsed.error).toBe('delegation_invalid');
283
+ expect(parsed.reason).toContain('statusListResolver');
284
+ });
285
+
286
+ it('should reject delegations whose audience does not include the server DID', async () => {
287
+ const { middleware: mcpi } = await createTestMiddleware();
288
+ const vc = await issueDelegationVC({
289
+ scopes: ['test:scope'],
290
+ audience: 'did:web:other.example.com',
291
+ });
292
+
293
+ const handler = mcpi.wrapWithDelegation(
294
+ 'my-tool',
295
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
296
+ async () => ({ content: [{ type: 'text', text: 'should not reach' }] }),
297
+ );
298
+
299
+ const result = await handler({ _mcpi_delegation: vc });
300
+
301
+ expect(result.isError).toBe(true);
302
+ const parsed = JSON.parse(result.content[0].text);
303
+ expect(parsed.error).toBe('delegation_invalid');
304
+ expect(parsed.reason).toContain('audience does not include server DID');
305
+ });
306
+
307
+ it('should reject parent delegations when no chain resolver is configured', async () => {
308
+ const { middleware: mcpi } = await createTestMiddleware();
309
+ const vc = await issueDelegationVC({
310
+ scopes: ['test:scope'],
311
+ parentId: 'parent-delegation',
312
+ });
313
+
314
+ const handler = mcpi.wrapWithDelegation(
315
+ 'my-tool',
316
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
317
+ async () => ({ content: [{ type: 'text', text: 'should not reach' }] }),
318
+ );
319
+
320
+ const result = await handler({ _mcpi_delegation: vc });
321
+
322
+ expect(result.isError).toBe(true);
323
+ const parsed = JSON.parse(result.content[0].text);
324
+ expect(parsed.error).toBe('delegation_invalid');
325
+ expect(parsed.reason).toContain('resolveDelegationChain');
326
+ });
327
+
328
+ it('should allow parent delegations in legacy mode without a chain resolver', async () => {
329
+ const { middleware: mcpi } = await createTestMiddleware({
330
+ delegation: { allowLegacyUnsafeDelegation: true },
331
+ });
332
+ const vc = await issueDelegationVC({
333
+ scopes: ['test:scope'],
334
+ parentId: 'parent-delegation',
335
+ });
336
+
337
+ const handler = mcpi.wrapWithDelegation(
338
+ 'my-tool',
339
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
340
+ async () => ({ content: [{ type: 'text', text: 'legacy-ok' }] }),
341
+ );
342
+
343
+ const result = await handler({ _mcpi_delegation: vc });
344
+ expect(result.isError).toBeUndefined();
345
+ expect(result.content[0].text).toBe('legacy-ok');
346
+ });
347
+
348
+ it('should reject delegation chains that widen parent scopes', async () => {
349
+ const parentIssuer = await createDelegationIssuer();
350
+ const childIssuer = await createDelegationIssuer();
351
+ const leafSubject = (await createDelegationIssuer()).did;
352
+ const parentVc = await issueDelegationVC({
353
+ issuer: parentIssuer,
354
+ scopes: ['test:scope'],
355
+ subjectDid: childIssuer.did,
356
+ });
357
+ const childVc = await issueDelegationVC({
358
+ issuer: childIssuer,
359
+ scopes: ['test:scope', 'admin:scope'],
360
+ parentId: parentVc.credentialSubject.delegation.id,
361
+ subjectDid: leafSubject,
362
+ });
363
+
364
+ const { middleware: mcpi } = await createTestMiddleware({
365
+ delegation: {
366
+ resolveDelegationChain: async () => [parentVc],
367
+ },
368
+ });
369
+
370
+ const handler = mcpi.wrapWithDelegation(
371
+ 'my-tool',
372
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
373
+ async () => ({ content: [{ type: 'text', text: 'should not reach' }] }),
374
+ );
375
+
376
+ const result = await handler({ _mcpi_delegation: childVc });
377
+
378
+ expect(result.isError).toBe(true);
379
+ const parsed = JSON.parse(result.content[0].text);
380
+ expect(parsed.error).toBe('delegation_invalid');
381
+ expect(parsed.reason).toContain('widens scopes');
382
+ });
383
+
384
+ it('should accept did:web issuers when a fetch-backed resolver is available', async () => {
385
+ const did = 'did:web:issuer.example.com';
386
+ const kid = `${did}#key-1`;
387
+ const issuer = await createDelegationIssuer({ did, kid });
388
+ const vc = await issueDelegationVC({
389
+ issuer,
390
+ scopes: ['test:scope'],
391
+ });
392
+ const fetchProvider = new MockFetchProvider();
393
+ fetchProvider.fetch = async () =>
394
+ new Response(
395
+ JSON.stringify({
396
+ id: did,
397
+ verificationMethod: [
398
+ {
399
+ id: kid,
400
+ type: 'Ed25519VerificationKey2020',
401
+ controller: did,
402
+ publicKeyJwk: {
403
+ kty: 'OKP',
404
+ crv: 'Ed25519',
405
+ x: base64urlEncodeFromBytes(base64ToBytes(issuer.keyPair.publicKey)),
406
+ },
407
+ },
408
+ ],
409
+ authentication: [kid],
410
+ assertionMethod: [kid],
411
+ }),
412
+ {
413
+ status: 200,
414
+ headers: { 'Content-Type': 'application/json' },
415
+ },
416
+ );
417
+
418
+ const { middleware: mcpi } = await createTestMiddleware({
419
+ delegation: { fetchProvider },
420
+ });
421
+
422
+ const handler = mcpi.wrapWithDelegation(
423
+ 'my-tool',
424
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
425
+ async (args) => ({
426
+ content: [{ type: 'text', text: `Called: ${JSON.stringify(args)}` }],
427
+ }),
428
+ );
429
+
430
+ const result = await handler({ _mcpi_delegation: vc, name: 'DIF' });
431
+
432
+ expect(result.isError).toBeUndefined();
433
+ const parsed = JSON.parse(result.content[0].text.replace('Called: ', ''));
434
+ expect(parsed['name']).toBe('DIF');
435
+ });
436
+
437
+ it('should allow credentialStatus without status resolver in legacy mode', async () => {
438
+ const { middleware: mcpi } = await createTestMiddleware({
439
+ delegation: { allowLegacyUnsafeDelegation: true },
440
+ });
441
+ const vc = await issueDelegationVC({
442
+ scopes: ['test:scope'],
443
+ credentialStatus: {
444
+ id: 'https://status.example.com/revocation/v1#0',
445
+ type: 'StatusList2021Entry',
446
+ statusPurpose: 'revocation',
447
+ statusListIndex: '0',
448
+ statusListCredential: 'https://status.example.com/revocation/v1',
449
+ },
450
+ });
451
+
452
+ const handler = mcpi.wrapWithDelegation(
453
+ 'my-tool',
454
+ { scopeId: 'test:scope', consentUrl: 'https://example.com/consent' },
455
+ async () => ({ content: [{ type: 'text', text: 'legacy-status-ok' }] }),
456
+ );
457
+
458
+ const result = await handler({ _mcpi_delegation: vc });
459
+ expect(result.isError).toBeUndefined();
460
+ expect(result.content[0].text).toBe('legacy-status-ok');
461
+ });
462
+ });
463
+
464
+ describe('autoSession', () => {
465
+ it('should auto-create session and attach proof without handshake', async () => {
466
+ const { middleware: mcpi } = await createTestMiddleware({ autoSession: true });
467
+
468
+ const handler = mcpi.wrapWithProof('greet', async (args) => ({
469
+ content: [{ type: 'text', text: `Hello, ${args['name']}!` }],
470
+ }));
471
+
472
+ // No handshake — call tool directly
473
+ const result = await handler({ name: 'DIF' });
474
+
475
+ expect(result.content).toHaveLength(1);
476
+ expect(result.content[0].text).toBe('Hello, DIF!');
477
+
478
+ // Proof should still be generated via auto-session
479
+ expect(result._meta).toBeDefined();
480
+ const proof = result._meta!.proof as { jws: string; meta: Record<string, unknown> };
481
+ expect(proof.jws).toBeDefined();
482
+ expect(proof.meta.did).toMatch(/^did:key:/);
483
+ expect(proof.meta.sessionId).toMatch(/^mcpi_/);
484
+ // Nonce is now a base64url-encoded 16-byte random value
485
+ expect(proof.meta.nonce).toMatch(/^[A-Za-z0-9_-]+$/);
486
+ });
487
+
488
+ it('should reuse auto-created session across multiple calls', async () => {
489
+ const { middleware: mcpi } = await createTestMiddleware({ autoSession: true });
490
+
491
+ const handler = mcpi.wrapWithProof('greet', async () => ({
492
+ content: [{ type: 'text', text: 'Hello!' }],
493
+ }));
494
+
495
+ const result1 = await handler({});
496
+ const result2 = await handler({});
497
+
498
+ const proof1 = result1._meta!.proof as { meta: Record<string, unknown> };
499
+ const proof2 = result2._meta!.proof as { meta: Record<string, unknown> };
500
+
501
+ expect(proof1.meta.sessionId).toBe(proof2.meta.sessionId);
502
+ });
503
+ });
504
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * MCP-I Middleware
3
+ *
4
+ * Provides identity-aware middleware for @modelcontextprotocol/sdk Server.
5
+ */
6
+
7
+ export {
8
+ createMCPIMiddleware,
9
+ type MCPIConfig,
10
+ type MCPIDelegationConfig,
11
+ type MCPIIdentityConfig,
12
+ type MCPIMiddleware,
13
+ type MCPIToolDefinition,
14
+ type MCPIToolHandler,
15
+ type MCPIServer,
16
+ } from './with-mcpi.js';