mcp-hosted-tools-list.test.mjs
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | /** |
| 2 | * Regression guard: hosted MCP tools/list must succeed for every role. |
| 3 | * A single bad Zod → JSON Schema export (e.g. z.record(z.unknown())) fails the entire list. |
| 4 | */ |
| 5 | |
| 6 | import { describe, it } from 'node:test'; |
| 7 | import assert from 'node:assert/strict'; |
| 8 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; |
| 9 | import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; |
| 10 | import { createHostedMcpServer } from '../hub/gateway/mcp-hosted-server.mjs'; |
| 11 | |
| 12 | const CANISTER_URL = 'http://canister.test:4322'; |
| 13 | const BRIDGE_URL = 'http://bridge.test:4321'; |
| 14 | |
| 15 | /** Golden sets: update when adding/removing tools in mcp-hosted-server.mjs */ |
| 16 | const TOOLS_VIEWER = [ |
| 17 | 'backlinks', |
| 18 | 'cluster', |
| 19 | 'enrich', |
| 20 | 'extract_tasks', |
| 21 | 'get_document_tree', |
| 22 | 'get_metadata_facets', |
| 23 | 'get_note', |
| 24 | 'get_note_outline', |
| 25 | 'get_section_source', |
| 26 | 'list_notes', |
| 27 | 'relate', |
| 28 | 'search', |
| 29 | 'summarize', |
| 30 | 'tag_suggest', |
| 31 | ]; |
| 32 | const TOOLS_EDITOR = [ |
| 33 | 'backlinks', |
| 34 | 'capture', |
| 35 | 'cluster', |
| 36 | 'enrich', |
| 37 | 'extract_tasks', |
| 38 | 'get_document_tree', |
| 39 | 'get_metadata_facets', |
| 40 | 'get_note', |
| 41 | 'get_note_outline', |
| 42 | 'get_section_source', |
| 43 | 'hub_create_proposal', |
| 44 | 'list_notes', |
| 45 | 'relate', |
| 46 | 'search', |
| 47 | 'summarize', |
| 48 | 'tag_suggest', |
| 49 | 'transcribe', |
| 50 | 'vault_sync', |
| 51 | 'write', |
| 52 | ]; |
| 53 | const TOOLS_ADMIN = [ |
| 54 | 'backlinks', |
| 55 | 'capture', |
| 56 | 'cluster', |
| 57 | 'enrich', |
| 58 | 'export', |
| 59 | 'extract_tasks', |
| 60 | 'get_document_tree', |
| 61 | 'get_metadata_facets', |
| 62 | 'get_note', |
| 63 | 'get_note_outline', |
| 64 | 'get_section_source', |
| 65 | 'hub_create_proposal', |
| 66 | 'import', |
| 67 | 'import_url', |
| 68 | 'index', |
| 69 | 'list_notes', |
| 70 | 'relate', |
| 71 | 'search', |
| 72 | 'summarize', |
| 73 | 'tag_suggest', |
| 74 | 'transcribe', |
| 75 | 'vault_sync', |
| 76 | 'write', |
| 77 | ]; |
| 78 | |
| 79 | function sortNames(names) { |
| 80 | return [...names].sort(); |
| 81 | } |
| 82 | |
| 83 | async function listToolNamesForRole(role) { |
| 84 | const mcpServer = createHostedMcpServer({ |
| 85 | userId: 'u-test', |
| 86 | vaultId: 'v-test', |
| 87 | role, |
| 88 | token: 'tok-test', |
| 89 | canisterUrl: CANISTER_URL, |
| 90 | bridgeUrl: BRIDGE_URL, |
| 91 | gatewayApiBaseUrl: 'http://gateway.test:5555', |
| 92 | }); |
| 93 | const client = new Client({ name: 'tools-list-test', version: '0.0.1' }); |
| 94 | const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| 95 | await mcpServer.connect(serverTransport); |
| 96 | await client.connect(clientTransport); |
| 97 | try { |
| 98 | const { tools } = await client.listTools(); |
| 99 | assert.ok(Array.isArray(tools), 'tools/list must return an array'); |
| 100 | assert.ok(tools.length > 0, `${role}: at least one tool must be listed`); |
| 101 | for (const t of tools) { |
| 102 | assert.ok(t.name, 'each tool has a name'); |
| 103 | assert.ok( |
| 104 | t.inputSchema != null && typeof t.inputSchema === 'object', |
| 105 | `tool ${t.name} must have inputSchema object (tools/list serialization)` |
| 106 | ); |
| 107 | } |
| 108 | return tools.map((t) => t.name); |
| 109 | } finally { |
| 110 | try { |
| 111 | await client.close(); |
| 112 | } catch (_) {} |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | describe('hosted MCP tools/list (JSON Schema export)', () => { |
| 117 | it('viewer role lists expected tools without throw', async () => { |
| 118 | const names = sortNames(await listToolNamesForRole('viewer')); |
| 119 | assert.deepEqual(names, TOOLS_VIEWER); |
| 120 | }); |
| 121 | |
| 122 | it('editor role lists expected tools without throw', async () => { |
| 123 | const names = sortNames(await listToolNamesForRole('editor')); |
| 124 | assert.deepEqual(names, TOOLS_EDITOR); |
| 125 | }); |
| 126 | |
| 127 | it('admin role lists expected tools without throw', async () => { |
| 128 | const names = sortNames(await listToolNamesForRole('admin')); |
| 129 | assert.deepEqual(names, TOOLS_ADMIN); |
| 130 | }); |
| 131 | |
| 132 | it('evaluator role lists same tools as editor (incl. hub_create_proposal)', async () => { |
| 133 | const names = sortNames(await listToolNamesForRole('evaluator')); |
| 134 | assert.deepEqual(names, TOOLS_EDITOR); |
| 135 | }); |
| 136 | }); |