mcp-hosted-cluster.test.mjs
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