@mcp-i/core 0.1.0 → 1.1.0-canary.2
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 +43 -20
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +5 -1
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +5 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/with-mcpi-server.d.ts +62 -0
- package/dist/middleware/with-mcpi-server.d.ts.map +1 -0
- package/dist/middleware/with-mcpi-server.js +94 -0
- package/dist/middleware/with-mcpi-server.js.map +1 -0
- package/dist/middleware/with-mcpi.d.ts +25 -10
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +21 -7
- package/dist/middleware/with-mcpi.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/node-crypto.d.ts +26 -0
- package/dist/providers/node-crypto.d.ts.map +1 -0
- package/dist/providers/node-crypto.js +69 -0
- package/dist/providers/node-crypto.js.map +1 -0
- package/package.json +8 -3
- package/src/__tests__/integration/mcp-enhance-server.test.ts +311 -0
- package/src/__tests__/integration/mcp-transport-context7.test.ts +413 -0
- package/src/__tests__/integration/mcp-transport.test.ts +390 -0
- package/src/index.ts +5 -0
- package/src/middleware/index.ts +10 -1
- package/src/middleware/with-mcpi-server.ts +185 -0
- package/src/middleware/with-mcpi.ts +35 -13
- package/src/providers/index.ts +2 -0
- package/src/providers/node-crypto.ts +107 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-crypto.d.ts","sourceRoot":"","sources":["../../src/providers/node-crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAWH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAW3C,qBAAa,kBAAmB,SAAQ,cAAc;IAC9C,IAAI,CACR,IAAI,EAAE,UAAU,EAChB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,UAAU,CAAC;IAgBhB,MAAM,CACV,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,UAAU,EACrB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,CAAC;IAsBb,eAAe,IAAI,OAAO,CAAC;QAC/B,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAYI,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAOvC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAGvD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js CryptoProvider
|
|
3
|
+
*
|
|
4
|
+
* Ed25519 crypto backed by Node.js built-in `node:crypto`.
|
|
5
|
+
* Use this on any Node.js 20+ server.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { NodeCryptoProvider } from '@mcp-i/core/providers';
|
|
10
|
+
* import { withMCPI } from '@mcp-i/core/middleware';
|
|
11
|
+
*
|
|
12
|
+
* await withMCPI(server, { crypto: new NodeCryptoProvider() });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, randomBytes, } from "node:crypto";
|
|
16
|
+
import { CryptoProvider } from "./base.js";
|
|
17
|
+
/** PKCS8 DER header for Ed25519 private keys (16 bytes) */
|
|
18
|
+
const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
19
|
+
/** SPKI DER header for Ed25519 public keys (12 bytes) */
|
|
20
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
21
|
+
export class NodeCryptoProvider extends CryptoProvider {
|
|
22
|
+
async sign(data, privateKeyBase64) {
|
|
23
|
+
const privateKey = Buffer.from(privateKeyBase64, "base64");
|
|
24
|
+
// Handle both raw 32-byte and full 64-byte Ed25519 keys
|
|
25
|
+
const keyBytes = privateKey.length === 64 ? privateKey.subarray(0, 32) : privateKey;
|
|
26
|
+
const keyObject = createPrivateKey({
|
|
27
|
+
key: Buffer.concat([ED25519_PKCS8_PREFIX, keyBytes]),
|
|
28
|
+
format: "der",
|
|
29
|
+
type: "pkcs8",
|
|
30
|
+
});
|
|
31
|
+
return new Uint8Array(sign(null, Buffer.from(data), keyObject));
|
|
32
|
+
}
|
|
33
|
+
async verify(data, signature, publicKeyBase64) {
|
|
34
|
+
try {
|
|
35
|
+
const keyObject = createPublicKey({
|
|
36
|
+
key: Buffer.concat([
|
|
37
|
+
ED25519_SPKI_PREFIX,
|
|
38
|
+
Buffer.from(publicKeyBase64, "base64"),
|
|
39
|
+
]),
|
|
40
|
+
format: "der",
|
|
41
|
+
type: "spki",
|
|
42
|
+
});
|
|
43
|
+
return verify(null, Buffer.from(data), keyObject, Buffer.from(signature));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async generateKeyPair() {
|
|
50
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
51
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
52
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
privateKey: privateKey.subarray(16, 48).toString("base64"),
|
|
56
|
+
publicKey: publicKey.subarray(12, 44).toString("base64"),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async hash(data) {
|
|
60
|
+
const hex = createHash("sha256")
|
|
61
|
+
.update(Buffer.from(data))
|
|
62
|
+
.digest("hex");
|
|
63
|
+
return `sha256:${hex}`;
|
|
64
|
+
}
|
|
65
|
+
async randomBytes(length) {
|
|
66
|
+
return new Uint8Array(randomBytes(length));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=node-crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-crypto.js","sourceRoot":"","sources":["../../src/providers/node-crypto.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-i/core",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0-canary.2",
|
|
4
|
+
"description": "MCP-I protocol reference implementation — delegation, proof, and session for Model Context Protocol Identity",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"example:inspector": "npx @modelcontextprotocol/inspector npx tsx examples/node-server/server.ts --stdio"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
+
"@mcp-i/core": "^1.1.0-canary.1",
|
|
65
66
|
"jose": "^5.6.3",
|
|
66
67
|
"json-canonicalize": "^2.0.0"
|
|
67
68
|
},
|
|
@@ -76,14 +77,18 @@
|
|
|
76
77
|
"devDependencies": {
|
|
77
78
|
"@eslint/js": "^10.0.1",
|
|
78
79
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
80
|
+
"@types/express": "^5.0.6",
|
|
79
81
|
"@types/node": "^20.14.9",
|
|
80
82
|
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
81
83
|
"@typescript-eslint/parser": "^8.57.0",
|
|
82
84
|
"@vitest/coverage-v8": "^2.0.0",
|
|
85
|
+
"commander": "^14.0.3",
|
|
83
86
|
"eslint": "^10.0.3",
|
|
87
|
+
"express": "^5.2.1",
|
|
84
88
|
"typescript": "^5.5.3",
|
|
85
89
|
"typescript-eslint": "^8.57.0",
|
|
86
|
-
"vitest": "^2.0.0"
|
|
90
|
+
"vitest": "^2.0.0",
|
|
91
|
+
"zod": "^4.3.6"
|
|
87
92
|
},
|
|
88
93
|
"keywords": [
|
|
89
94
|
"mcp-i",
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* withMCPI() Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the dream API: `withMCPI(server, { crypto })` auto-registers
|
|
5
|
+
* the handshake tool and auto-attaches proofs to all tool responses.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
11
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { withMCPI, generateIdentity } from '../../middleware/with-mcpi-server.js';
|
|
14
|
+
import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
|
|
15
|
+
|
|
16
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
async function createTestPair(options?: {
|
|
19
|
+
proofAllTools?: boolean;
|
|
20
|
+
excludeTools?: string[];
|
|
21
|
+
autoSession?: boolean;
|
|
22
|
+
registerToolsBeforeWithMCPI?: boolean;
|
|
23
|
+
}) {
|
|
24
|
+
const crypto = new NodeCryptoProvider();
|
|
25
|
+
const server = new McpServer(
|
|
26
|
+
{ name: 'withMCPI-test', version: '1.0.0' },
|
|
27
|
+
{ instructions: 'Test server for withMCPI integration' },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Register tools BEFORE withMCPI to test pre-existing tool interception
|
|
31
|
+
if (options?.registerToolsBeforeWithMCPI) {
|
|
32
|
+
server.registerTool(
|
|
33
|
+
'greet',
|
|
34
|
+
{
|
|
35
|
+
description: 'Greet someone',
|
|
36
|
+
inputSchema: { name: z.string() },
|
|
37
|
+
},
|
|
38
|
+
async ({ name }) => ({
|
|
39
|
+
content: [{ type: 'text', text: `Hello, ${name}!` }],
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mcpi = await withMCPI(server, {
|
|
45
|
+
crypto,
|
|
46
|
+
autoSession: options?.autoSession ?? true,
|
|
47
|
+
proofAllTools: options?.proofAllTools,
|
|
48
|
+
excludeTools: options?.excludeTools,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Register tools AFTER withMCPI to test late registration
|
|
52
|
+
if (!options?.registerToolsBeforeWithMCPI) {
|
|
53
|
+
server.registerTool(
|
|
54
|
+
'greet',
|
|
55
|
+
{
|
|
56
|
+
description: 'Greet someone',
|
|
57
|
+
inputSchema: { name: z.string() },
|
|
58
|
+
},
|
|
59
|
+
async ({ name }) => ({
|
|
60
|
+
content: [{ type: 'text', text: `Hello, ${name}!` }],
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
server.registerTool(
|
|
66
|
+
'add',
|
|
67
|
+
{
|
|
68
|
+
description: 'Add two numbers',
|
|
69
|
+
inputSchema: { a: z.number(), b: z.number() },
|
|
70
|
+
},
|
|
71
|
+
async ({ a, b }) => ({
|
|
72
|
+
content: [{ type: 'text', text: `${a + b}` }],
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const client = new Client(
|
|
77
|
+
{ name: 'withMCPI-test-client', version: '1.0.0' },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
81
|
+
await server.connect(serverTransport);
|
|
82
|
+
await client.connect(clientTransport);
|
|
83
|
+
|
|
84
|
+
return { client, server, mcpi, crypto };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Tests ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe('withMCPI()', () => {
|
|
90
|
+
const pairs: Array<{ client: Client; server: McpServer }> = [];
|
|
91
|
+
|
|
92
|
+
afterEach(async () => {
|
|
93
|
+
for (const pair of pairs) {
|
|
94
|
+
await pair.client.close();
|
|
95
|
+
await pair.server.close();
|
|
96
|
+
}
|
|
97
|
+
pairs.length = 0;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
async function create(options?: Parameters<typeof createTestPair>[0]) {
|
|
101
|
+
const pair = await createTestPair(options);
|
|
102
|
+
pairs.push(pair);
|
|
103
|
+
return pair;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
it('auto-registers _mcpi_handshake tool', async () => {
|
|
107
|
+
const { client } = await create();
|
|
108
|
+
|
|
109
|
+
const result = await client.listTools();
|
|
110
|
+
const toolNames = result.tools.map((t) => t.name);
|
|
111
|
+
|
|
112
|
+
expect(toolNames).toContain('_mcpi_handshake');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('auto-proofs all registered tools', async () => {
|
|
116
|
+
const { client } = await create();
|
|
117
|
+
|
|
118
|
+
const result = await client.callTool({
|
|
119
|
+
name: 'greet',
|
|
120
|
+
arguments: { name: 'World' },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const first = result.content[0] as { type: string; text: string };
|
|
124
|
+
expect(first.text).toBe('Hello, World!');
|
|
125
|
+
|
|
126
|
+
// Proof should be present via auto-session + auto-proof
|
|
127
|
+
expect(result._meta).toBeDefined();
|
|
128
|
+
const proof = (result._meta as Record<string, unknown>).proof as {
|
|
129
|
+
jws: string;
|
|
130
|
+
meta: Record<string, unknown>;
|
|
131
|
+
};
|
|
132
|
+
expect(proof).toBeDefined();
|
|
133
|
+
expect(proof.jws).toBeDefined();
|
|
134
|
+
expect(proof.meta.did).toMatch(/^did:key:/);
|
|
135
|
+
expect(proof.meta.sessionId).toMatch(/^mcpi_/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('tools registered before withMCPI also get proofs', async () => {
|
|
139
|
+
const { client } = await create({ registerToolsBeforeWithMCPI: true });
|
|
140
|
+
|
|
141
|
+
const result = await client.callTool({
|
|
142
|
+
name: 'greet',
|
|
143
|
+
arguments: { name: 'Early Bird' },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const first = result.content[0] as { type: string; text: string };
|
|
147
|
+
expect(first.text).toBe('Hello, Early Bird!');
|
|
148
|
+
|
|
149
|
+
expect(result._meta).toBeDefined();
|
|
150
|
+
const proof = (result._meta as Record<string, unknown>).proof as {
|
|
151
|
+
jws: string;
|
|
152
|
+
};
|
|
153
|
+
expect(proof).toBeDefined();
|
|
154
|
+
expect(proof.jws).toBeDefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('excludeTools skips proof for specified tools', async () => {
|
|
158
|
+
const { client } = await create({ excludeTools: ['greet'] });
|
|
159
|
+
|
|
160
|
+
// 'greet' should NOT have proof
|
|
161
|
+
const greetResult = await client.callTool({
|
|
162
|
+
name: 'greet',
|
|
163
|
+
arguments: { name: 'No Proof' },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const greetContent = greetResult.content[0] as { type: string; text: string };
|
|
167
|
+
expect(greetContent.text).toBe('Hello, No Proof!');
|
|
168
|
+
|
|
169
|
+
// _meta may be undefined or proof should not be present
|
|
170
|
+
const greetProof = (greetResult._meta as Record<string, unknown> | undefined)?.proof;
|
|
171
|
+
expect(greetProof).toBeUndefined();
|
|
172
|
+
|
|
173
|
+
// 'add' should still have proof
|
|
174
|
+
const addResult = await client.callTool({
|
|
175
|
+
name: 'add',
|
|
176
|
+
arguments: { a: 2, b: 3 },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(addResult._meta).toBeDefined();
|
|
180
|
+
const addProof = (addResult._meta as Record<string, unknown>).proof as {
|
|
181
|
+
jws: string;
|
|
182
|
+
};
|
|
183
|
+
expect(addProof).toBeDefined();
|
|
184
|
+
expect(addProof.jws).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('proofAllTools: false disables auto-proofing', async () => {
|
|
188
|
+
const { client } = await create({ proofAllTools: false });
|
|
189
|
+
|
|
190
|
+
const result = await client.callTool({
|
|
191
|
+
name: 'greet',
|
|
192
|
+
arguments: { name: 'No Auto-Proof' },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const first = result.content[0] as { type: string; text: string };
|
|
196
|
+
expect(first.text).toBe('Hello, No Auto-Proof!');
|
|
197
|
+
|
|
198
|
+
// No proof — auto-proofing is off
|
|
199
|
+
const proof = (result._meta as Record<string, unknown> | undefined)?.proof;
|
|
200
|
+
expect(proof).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('handshake establishes session through withMCPI', async () => {
|
|
204
|
+
const { client, mcpi } = await create({ autoSession: false });
|
|
205
|
+
|
|
206
|
+
const result = await client.callTool({
|
|
207
|
+
name: '_mcpi_handshake',
|
|
208
|
+
arguments: {
|
|
209
|
+
nonce: `test-${Date.now()}`,
|
|
210
|
+
audience: mcpi.identity.did,
|
|
211
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const first = result.content[0] as { type: string; text: string };
|
|
216
|
+
const parsed = JSON.parse(first.text);
|
|
217
|
+
expect(parsed.success).toBe(true);
|
|
218
|
+
expect(parsed.sessionId).toMatch(/^mcpi_/);
|
|
219
|
+
expect(parsed.serverDid).toBe(mcpi.identity.did);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('multiple tools share the same auto-session', async () => {
|
|
223
|
+
const { client } = await create();
|
|
224
|
+
|
|
225
|
+
const result1 = await client.callTool({
|
|
226
|
+
name: 'greet',
|
|
227
|
+
arguments: { name: 'Alice' },
|
|
228
|
+
});
|
|
229
|
+
const result2 = await client.callTool({
|
|
230
|
+
name: 'add',
|
|
231
|
+
arguments: { a: 1, b: 2 },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const proof1 = (result1._meta as Record<string, unknown>).proof as {
|
|
235
|
+
meta: Record<string, unknown>;
|
|
236
|
+
};
|
|
237
|
+
const proof2 = (result2._meta as Record<string, unknown>).proof as {
|
|
238
|
+
meta: Record<string, unknown>;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
expect(proof1.meta.sessionId).toBe(proof2.meta.sessionId);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('wrapWithDelegation still works alongside withMCPI', async () => {
|
|
245
|
+
const crypto = new NodeCryptoProvider();
|
|
246
|
+
const server = new McpServer(
|
|
247
|
+
{ name: 'delegation-test', version: '1.0.0' },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const mcpi = await withMCPI(server, { crypto, autoSession: true });
|
|
251
|
+
|
|
252
|
+
// Use wrapWithDelegation for a restricted tool
|
|
253
|
+
const restrictedHandler = mcpi.wrapWithDelegation(
|
|
254
|
+
'restricted-tool',
|
|
255
|
+
{ scopeId: 'admin:write', consentUrl: 'https://example.com/consent' },
|
|
256
|
+
async (args) => ({
|
|
257
|
+
content: [{ type: 'text', text: `Restricted: ${(args as { data: string }).data}` }],
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
server.registerTool(
|
|
262
|
+
'restricted-tool',
|
|
263
|
+
{
|
|
264
|
+
description: 'Requires delegation',
|
|
265
|
+
inputSchema: { data: z.string() },
|
|
266
|
+
},
|
|
267
|
+
async (args) => restrictedHandler(args as Record<string, unknown>),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const client = new Client({ name: 'delegation-client', version: '1.0.0' });
|
|
271
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
272
|
+
await server.connect(serverTransport);
|
|
273
|
+
await client.connect(clientTransport);
|
|
274
|
+
pairs.push({ client, server });
|
|
275
|
+
|
|
276
|
+
// Call without delegation — should get needs_authorization
|
|
277
|
+
const result = await client.callTool({
|
|
278
|
+
name: 'restricted-tool',
|
|
279
|
+
arguments: { data: 'test' },
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const first = result.content[0] as { type: string; text: string };
|
|
283
|
+
const parsed = JSON.parse(first.text);
|
|
284
|
+
expect(parsed.error).toBe('needs_authorization');
|
|
285
|
+
expect(parsed.authorizationUrl).toBe('https://example.com/consent');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('generateIdentity()', () => {
|
|
290
|
+
it('returns valid DID identity', async () => {
|
|
291
|
+
const crypto = new NodeCryptoProvider();
|
|
292
|
+
const identity = await generateIdentity(crypto);
|
|
293
|
+
|
|
294
|
+
expect(identity.did).toMatch(/^did:key:z6Mk/);
|
|
295
|
+
expect(identity.kid).toBe(`${identity.did}#keys-1`);
|
|
296
|
+
expect(identity.privateKey).toBeDefined();
|
|
297
|
+
expect(identity.publicKey).toBeDefined();
|
|
298
|
+
// Keys should be base64 strings
|
|
299
|
+
expect(() => atob(identity.privateKey)).not.toThrow();
|
|
300
|
+
expect(() => atob(identity.publicKey)).not.toThrow();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('generates unique identities each call', async () => {
|
|
304
|
+
const crypto = new NodeCryptoProvider();
|
|
305
|
+
const id1 = await generateIdentity(crypto);
|
|
306
|
+
const id2 = await generateIdentity(crypto);
|
|
307
|
+
|
|
308
|
+
expect(id1.did).not.toBe(id2.did);
|
|
309
|
+
expect(id1.privateKey).not.toBe(id2.privateKey);
|
|
310
|
+
});
|
|
311
|
+
});
|