mcp-hosted-export.test.mjs
173 lines 5.8 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted MCP export — GET canister /api/v1/export (parity with hub/bridge/server.mjs backup fetch).
3 */
4
5 import { describe, it, beforeEach, 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 BRIDGE_URL = 'http://bridge.test:4321';
12 const CANISTER_URL = 'http://canister.test:4322';
13
14 function makeCtx(overrides = {}) {
15 return {
16 userId: 'u-export',
17 vaultId: 'v-export',
18 role: 'admin',
19 token: 'tok-export',
20 canisterUrl: CANISTER_URL,
21 bridgeUrl: BRIDGE_URL,
22 canisterAuthSecret: 'gw-secret',
23 ...overrides,
24 };
25 }
26
27 function installFetchMock(handler) {
28 const calls = [];
29 const origFetch = globalThis.fetch;
30 globalThis.fetch = async (url, init) => {
31 calls.push({ url: String(url), init });
32 return handler(url, init, calls);
33 };
34 return {
35 calls,
36 restore() {
37 globalThis.fetch = origFetch;
38 },
39 };
40 }
41
42 describe('hosted MCP export — canister request shape', () => {
43 let mock;
44 let client;
45
46 beforeEach(() => {
47 const notes = [{ path: 'a.md', frontmatter: '{}', body: 'x' }];
48 mock = installFetchMock(() => ({
49 ok: true,
50 status: 200,
51 arrayBuffer: async () => new TextEncoder().encode(JSON.stringify({ notes })).buffer,
52 json: async () => ({ notes }),
53 text: async () => JSON.stringify({ notes }),
54 }));
55 });
56
57 afterEach(async () => {
58 mock.restore();
59 try {
60 await client?.close();
61 } catch (_) {}
62 });
63
64 it('sends GET to {canisterUrl}/api/v1/export', async () => {
65 const mcpServer = createHostedMcpServer(makeCtx());
66 client = new Client({ name: 'export-test', version: '0.0.1' });
67 const [ct, st] = InMemoryTransport.createLinkedPair();
68 await mcpServer.connect(st);
69 await client.connect(ct);
70 await client.callTool({ name: 'export', arguments: {} });
71
72 assert.equal(mock.calls.length, 1);
73 assert.equal(mock.calls[0].url, `${CANISTER_URL}/api/v1/export`);
74 assert.equal(mock.calls[0].init.method, 'GET');
75 });
76
77 it('sends Authorization, X-Vault-Id, X-User-Id, X-Gateway-Auth', async () => {
78 const mcpServer = createHostedMcpServer(makeCtx());
79 client = new Client({ name: 'export-test', version: '0.0.1' });
80 const [ct, st] = InMemoryTransport.createLinkedPair();
81 await mcpServer.connect(st);
82 await client.connect(ct);
83 await client.callTool({ name: 'export', arguments: {} });
84
85 const h = mock.calls[0].init.headers;
86 assert.equal(h['Authorization'], 'Bearer tok-export');
87 assert.equal(h['X-Vault-Id'], 'v-export');
88 assert.equal(h['X-User-Id'], 'u-export');
89 assert.equal(h['X-Gateway-Auth'], 'gw-secret');
90 });
91
92 it('returns parsed notes JSON as tool content', async () => {
93 const mcpServer = createHostedMcpServer(makeCtx());
94 client = new Client({ name: 'export-test', version: '0.0.1' });
95 const [ct, st] = InMemoryTransport.createLinkedPair();
96 await mcpServer.connect(st);
97 await client.connect(ct);
98 const result = await client.callTool({ name: 'export', arguments: {} });
99
100 assert.ok(result.content?.length);
101 const parsed = JSON.parse(result.content[0].text);
102 assert.equal(parsed.notes.length, 1);
103 assert.equal(parsed.notes[0].path, 'a.md');
104 });
105
106 it('returns EXPORT_TOO_LARGE when body exceeds cap', async () => {
107 mock.restore();
108 const huge = 'x'.repeat(5 * 1024 * 1024);
109 mock = installFetchMock(() => ({
110 ok: true,
111 status: 200,
112 arrayBuffer: async () => new TextEncoder().encode(JSON.stringify({ notes: [{ body: huge }] })).buffer,
113 json: async () => ({ notes: [{ body: huge }] }),
114 text: async () => JSON.stringify({ notes: [{ body: huge }] }),
115 }));
116
117 const mcpServer = createHostedMcpServer(makeCtx());
118 client = new Client({ name: 'export-test', version: '0.0.1' });
119 const [ct, st] = InMemoryTransport.createLinkedPair();
120 await mcpServer.connect(st);
121 await client.connect(ct);
122 const result = await client.callTool({ name: 'export', arguments: {} });
123
124 assert.equal(result.isError, true);
125 const err = JSON.parse(result.content[0].text);
126 assert.equal(err.code, 'EXPORT_TOO_LARGE');
127 assert.ok(String(err.error).includes('MCP-only'));
128 });
129
130 it('returns isError on upstream HTTP failure', async () => {
131 mock.restore();
132 mock = installFetchMock(() => ({
133 ok: false,
134 status: 403,
135 arrayBuffer: async () => new TextEncoder().encode('{"error":"Gateway authentication required"}').buffer,
136 text: async () => '{"error":"Gateway authentication required"}',
137 }));
138
139 const mcpServer = createHostedMcpServer(makeCtx());
140 client = new Client({ name: 'export-test', version: '0.0.1' });
141 const [ct, st] = InMemoryTransport.createLinkedPair();
142 await mcpServer.connect(st);
143 await client.connect(ct);
144 const result = await client.callTool({ name: 'export', arguments: {} });
145
146 assert.equal(result.isError, true);
147 const err = JSON.parse(result.content[0].text);
148 assert.equal(err.code, 'UPSTREAM_ERROR');
149 });
150
151 it('viewer role does not register export', async () => {
152 mock.restore();
153 mock = installFetchMock(() => ({
154 ok: true,
155 status: 200,
156 arrayBuffer: async () => new TextEncoder().encode('{}').buffer,
157 json: async () => ({}),
158 text: async () => '{}',
159 }));
160 const mcpServer = createHostedMcpServer(makeCtx({ role: 'viewer' }));
161 const c = new Client({ name: 'export-viewer', version: '0.0.1' });
162 const [ct, st] = InMemoryTransport.createLinkedPair();
163 await mcpServer.connect(st);
164 await c.connect(ct);
165 try {
166 const { tools } = await c.listTools();
167 assert.ok(!tools.some((t) => t.name === 'export'));
168 } finally {
169 await c.close();
170 mock.restore();
171 }
172 });
173 });
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