mcp-hosted-backlinks.test.mjs
144 lines 4.1 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted MCP `backlinks` — canister list + per-note GET + wikilink scan.
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 installBacklinksFetchMock() {
27 const calls = [];
28 const origFetch = globalThis.fetch;
29 globalThis.fetch = async (url) => {
30 const u = String(url);
31 calls.push({ url: u });
32 if (u === `${CANISTER_URL}/api/v1/notes/target.md`) {
33 return {
34 ok: true,
35 status: 200,
36 json: async () => ({ path: 'target.md', body: 'target body', frontmatter: '{}' }),
37 text: async () => '{}',
38 };
39 }
40 if (u.includes(`${CANISTER_URL}/api/v1/notes?`)) {
41 const offset = u.includes('offset=1') ? 1 : 0;
42 if (offset === 0) {
43 return {
44 ok: true,
45 status: 200,
46 json: async () => ({
47 notes: [
48 { path: 'target.md', frontmatter: '{}' },
49 { path: 'linker.md', frontmatter: '{}' },
50 ],
51 total: 2,
52 }),
53 text: async () => '{}',
54 };
55 }
56 return {
57 ok: true,
58 status: 200,
59 json: async () => ({ notes: [], total: 2 }),
60 text: async () => '{}',
61 };
62 }
63 if (u.includes(`${CANISTER_URL}/api/v1/notes/`) && u.includes('linker.md')) {
64 return {
65 ok: true,
66 status: 200,
67 json: async () => ({
68 path: 'linker.md',
69 body: 'See [[target]] here.',
70 frontmatter: '{"title":"Linker"}',
71 }),
72 text: async () => '{}',
73 };
74 }
75 return {
76 ok: false,
77 status: 404,
78 json: async () => ({}),
79 text: async () => 'not found',
80 };
81 };
82 return {
83 calls,
84 restore() {
85 globalThis.fetch = origFetch;
86 },
87 };
88 }
89
90 async function connectPair(ctx) {
91 const mcpServer = createHostedMcpServer(ctx ?? makeCtx());
92 const client = new Client({ name: 'backlinks-test', version: '0.0.1' });
93 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
94 await mcpServer.connect(serverTransport);
95 await client.connect(clientTransport);
96 return { client, mcpServer };
97 }
98
99 describe('hosted MCP backlinks', () => {
100 let mock = { restore() {} };
101
102 afterEach(() => {
103 mock.restore();
104 });
105
106 it('returns backlinks when another note wikilinks to target basename', async () => {
107 mock = installBacklinksFetchMock();
108 const { client } = await connectPair();
109
110 const result = await client.callTool({
111 name: 'backlinks',
112 arguments: { path: 'target.md' },
113 });
114
115 assert.ok(!result.isError);
116 const out = JSON.parse(result.content[0].text);
117 assert.equal(out.path, 'target.md');
118 assert.equal(out.backlinks.length, 1);
119 assert.equal(out.backlinks[0].path, 'linker.md');
120 assert.ok(out.backlinks[0].context.includes('[[target]]'));
121 assert.equal(out.backlinks_truncated, false);
122 assert.ok(typeof out.backlinks_notes_scanned === 'number');
123 });
124
125 it('returns isError when target note missing', async () => {
126 const origFetch = globalThis.fetch;
127 globalThis.fetch = async (url) => {
128 const u = String(url);
129 if (u.includes('/notes/missing.md') && !u.includes('?')) {
130 return { ok: false, status: 404, json: async () => ({}), text: async () => 'nf' };
131 }
132 return { ok: false, status: 500, json: async () => ({}), text: async () => '' };
133 };
134 mock = { restore: () => { globalThis.fetch = origFetch; } };
135 const { client } = await connectPair();
136
137 const result = await client.callTool({
138 name: 'backlinks',
139 arguments: { path: 'missing.md' },
140 });
141
142 assert.equal(result.isError, true);
143 });
144 });
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