mcp-hosted-cluster.test.mjs
151 lines 4.4 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted MCP `cluster` — canister list + bodies + bridge POST /api/v1/embed + k-means.
3 */
4
5 import { describe, it, afterEach } from 'node:test';
6 import assert from 'node:assert/strict';
7 import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8 import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
9 import { createHostedMcpServer } from '../hub/gateway/mcp-hosted-server.mjs';
10
11 const CANISTER_URL = 'http://canister.test:4322';
12 const BRIDGE_URL = 'http://bridge.test:4321';
13
14 function makeCtx(overrides = {}) {
15 return {
16 userId: 'u-1',
17 vaultId: 'v-1',
18 role: 'viewer',
19 token: 'tok-test',
20 canisterUrl: CANISTER_URL,
21 bridgeUrl: BRIDGE_URL,
22 ...overrides,
23 };
24 }
25
26 function installClusterFetchMock({ shortList = false } = {}) {
27 const calls = [];
28 const origFetch = globalThis.fetch;
29 globalThis.fetch = async (url, init) => {
30 const u = String(url);
31 calls.push({ url: u, method: init?.method, body: init?.body });
32 if (u.includes(`${CANISTER_URL}/api/v1/notes?`)) {
33 const notes = shortList
34 ? [
35 {
36 path: 'a.md',
37 frontmatter: '{"title":"Alpha"}',
38 body: 'hello world one',
39 },
40 ]
41 : [
42 {
43 path: 'a.md',
44 frontmatter: '{"title":"Alpha"}',
45 body: 'hello world one',
46 },
47 {
48 path: 'b.md',
49 frontmatter: '{"title":"Beta"}',
50 body: 'hello world two',
51 },
52 {
53 path: 'c.md',
54 frontmatter: '{}',
55 body: 'hello world three',
56 },
57 ];
58 return {
59 ok: true,
60 status: 200,
61 json: async () => ({ notes, total: notes.length }),
62 text: async () => '{}',
63 };
64 }
65 if (u.includes(`${BRIDGE_URL}/api/v1/embed`)) {
66 const body = init?.body ? JSON.parse(String(init.body)) : {};
67 const n = Array.isArray(body.texts) ? body.texts.length : 0;
68 const vectors = Array.from({ length: n }, (_, i) => {
69 const v = [0, 0, 0];
70 v[i % 3] = 1;
71 return v;
72 });
73 return {
74 ok: true,
75 status: 200,
76 json: async () => ({ vectors, embedding_input_tokens: 3, texts_count: n }),
77 text: async () => '{}',
78 };
79 }
80 return {
81 ok: false,
82 status: 404,
83 json: async () => ({}),
84 text: async () => 'not found',
85 };
86 };
87 return {
88 calls,
89 restore() {
90 globalThis.fetch = origFetch;
91 },
92 };
93 }
94
95 async function connectPair(ctx) {
96 const mcpServer = createHostedMcpServer(ctx ?? makeCtx());
97 const client = new Client({ name: 'cluster-test', version: '0.0.1' });
98 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
99 await mcpServer.connect(serverTransport);
100 await client.connect(clientTransport);
101 return { client, mcpServer };
102 }
103
104 describe('hosted MCP cluster', () => {
105 let mock = { restore() {} };
106
107 afterEach(() => {
108 mock.restore();
109 });
110
111 it('POSTs texts to bridge /api/v1/embed and returns clusters', async () => {
112 mock = installClusterFetchMock();
113 const { client } = await connectPair();
114
115 const result = await client.callTool({
116 name: 'cluster',
117 arguments: { n_clusters: 3 },
118 });
119
120 assert.ok(!result.isError);
121 const out = JSON.parse(result.content[0].text);
122 assert.equal(out.notes_sampled, 3);
123 assert.equal(out.max_notes, 200);
124 assert.equal(out.clusters.length, 3);
125 const allPaths = out.clusters.flatMap((c) => c.paths);
126 assert.ok(allPaths.includes('a.md'));
127 assert.ok(allPaths.includes('b.md'));
128 assert.ok(allPaths.includes('c.md'));
129 assert.equal(out.cluster_truncated, false);
130 assert.ok(mock.calls.some((c) => c.url.includes('/api/v1/embed')));
131 const embedCall = mock.calls.find((c) => c.url.includes('/api/v1/embed'));
132 const payload = JSON.parse(String(embedCall.body));
133 assert.equal(payload.texts.length, 3);
134 });
135
136 it('returns note when too few notes for k', async () => {
137 mock = installClusterFetchMock({ shortList: true });
138 const { client } = await connectPair();
139
140 const result = await client.callTool({
141 name: 'cluster',
142 arguments: { n_clusters: 5 },
143 });
144
145 assert.ok(!result.isError);
146 const out = JSON.parse(result.content[0].text);
147 assert.deepEqual(out.clusters, []);
148 assert.ok(out.note.includes('Not enough notes'));
149 assert.equal(out.notes_sampled, 1);
150 });
151 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago