mcp-hosted-hub-create-proposal.test.mjs
179 lines 6.3 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted MCP hub_create_proposal: POST gateway /api/v1/proposals with JWT + X-Vault-Id (mocked fetch).
3 */
4
5 import { describe, it } 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 const GATEWAY_BASE = 'http://gateway.test:5555';
14
15 describe('hosted MCP hub_create_proposal', () => {
16 it('POSTs JSON to gateway /api/v1/proposals with Bearer and X-Vault-Id', async () => {
17 const calls = [];
18 const origFetch = globalThis.fetch;
19 globalThis.fetch = async (url, init) => {
20 calls.push({ url: String(url), init });
21 if (String(url) === `${GATEWAY_BASE}/api/v1/proposals` && init?.method === 'POST') {
22 return {
23 ok: true,
24 status: 200,
25 async text() {
26 return JSON.stringify({
27 proposal_id: 'prop-test-1',
28 path: 'inbox/x.md',
29 status: 'proposed',
30 });
31 },
32 };
33 }
34 return origFetch(url, init);
35 };
36 try {
37 const mcpServer = createHostedMcpServer({
38 userId: 'u1',
39 vaultId: 'vault-a',
40 role: 'editor',
41 token: 'jwt-abc',
42 canisterUrl: CANISTER_URL,
43 bridgeUrl: BRIDGE_URL,
44 gatewayApiBaseUrl: GATEWAY_BASE,
45 });
46 const client = new Client({ name: 'hub-proposal-test', version: '0.0.1' });
47 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
48 await mcpServer.connect(serverTransport);
49 await client.connect(clientTransport);
50 try {
51 const out = await client.callTool({
52 name: 'hub_create_proposal',
53 arguments: { path: 'inbox/x.md', body: 'hello', intent: 'test' },
54 });
55 assert.equal(calls.length, 1);
56 assert.equal(calls[0].url, `${GATEWAY_BASE}/api/v1/proposals`);
57 assert.equal(calls[0].init.method, 'POST');
58 const h = calls[0].init.headers;
59 const auth =
60 typeof h.get === 'function' ? h.get('authorization') : h['authorization'] || h['Authorization'];
61 const vault =
62 typeof h.get === 'function' ? h.get('x-vault-id') : h['x-vault-id'] || h['X-Vault-Id'];
63 assert.match(String(auth || ''), /Bearer\s+jwt-abc/i);
64 assert.equal(String(vault || ''), 'vault-a');
65 const posted = JSON.parse(calls[0].init.body);
66 assert.equal(posted.path, 'inbox/x.md');
67 assert.equal(posted.body, 'hello');
68 assert.equal(posted.intent, 'test');
69 assert.deepEqual(posted.frontmatter, {});
70 const text = out.content?.[0]?.type === 'text' ? out.content[0].text : '';
71 const data = JSON.parse(text);
72 assert.equal(data.proposal_id, 'prop-test-1');
73 assert.equal(data.path, 'inbox/x.md');
74 assert.ok(!out.isError);
75 } finally {
76 try {
77 await client.close();
78 } catch (_) {}
79 }
80 } finally {
81 globalThis.fetch = origFetch;
82 }
83 });
84
85 it('returns structured MCP error when Hub responds 400 with JSON', async () => {
86 const origFetch = globalThis.fetch;
87 globalThis.fetch = async () => ({
88 ok: false,
89 status: 400,
90 async text() {
91 return JSON.stringify({ error: 'path invalid', code: 'BAD_REQUEST', detail: 'no dots' });
92 },
93 });
94 try {
95 const mcpServer = createHostedMcpServer({
96 userId: 'u1',
97 vaultId: 'default',
98 role: 'editor',
99 token: 'jwt',
100 canisterUrl: CANISTER_URL,
101 bridgeUrl: BRIDGE_URL,
102 gatewayApiBaseUrl: GATEWAY_BASE,
103 });
104 const client = new Client({ name: 'hub-proposal-err', version: '0.0.1' });
105 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
106 await mcpServer.connect(serverTransport);
107 await client.connect(clientTransport);
108 try {
109 const out = await client.callTool({
110 name: 'hub_create_proposal',
111 arguments: { path: 'bad/../x.md', body: '' },
112 });
113 assert.equal(out.isError, true);
114 const text = out.content?.[0]?.type === 'text' ? out.content[0].text : '';
115 const data = JSON.parse(text);
116 assert.equal(data.error, 'path invalid');
117 assert.equal(data.code, 'BAD_REQUEST');
118 assert.equal(data.http_status, 400);
119 assert.equal(data.detail, 'no dots');
120 } finally {
121 try {
122 await client.close();
123 } catch (_) {}
124 }
125 } finally {
126 globalThis.fetch = origFetch;
127 }
128 });
129
130 it('is registered for evaluator when gatewayApiBaseUrl is set', async () => {
131 const mcpServer = createHostedMcpServer({
132 userId: 'u1',
133 vaultId: 'default',
134 role: 'evaluator',
135 token: 'jwt',
136 canisterUrl: CANISTER_URL,
137 bridgeUrl: BRIDGE_URL,
138 gatewayApiBaseUrl: GATEWAY_BASE,
139 });
140 const client = new Client({ name: 'evaluator-tools', version: '0.0.1' });
141 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
142 await mcpServer.connect(serverTransport);
143 await client.connect(clientTransport);
144 try {
145 const { tools } = await client.listTools();
146 const names = tools.map((t) => t.name);
147 assert.ok(names.includes('hub_create_proposal'));
148 } finally {
149 try {
150 await client.close();
151 } catch (_) {}
152 }
153 });
154
155 it('is not registered for viewer even with gatewayApiBaseUrl', async () => {
156 const mcpServer = createHostedMcpServer({
157 userId: 'u1',
158 vaultId: 'default',
159 role: 'viewer',
160 token: 'jwt',
161 canisterUrl: CANISTER_URL,
162 bridgeUrl: BRIDGE_URL,
163 gatewayApiBaseUrl: GATEWAY_BASE,
164 });
165 const client = new Client({ name: 'viewer-tools', version: '0.0.1' });
166 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
167 await mcpServer.connect(serverTransport);
168 await client.connect(clientTransport);
169 try {
170 const { tools } = await client.listTools();
171 const names = tools.map((t) => t.name);
172 assert.ok(!names.includes('hub_create_proposal'));
173 } finally {
174 try {
175 await client.close();
176 } catch (_) {}
177 }
178 });
179 });
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