mcp-section-source.test.mjs file-level

at sha256:0 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:9 feat(calendar): hosted bridge/gateway route parity and timeline noteRec… · aaronrene · Jun 19, 2026
1 /**
2 * Self-hosted MCP tests for get_section_source.
3 *
4 * Phase 1G mirrors the approved CLI SectionSource v0 contract over self-hosted
5 * MCP only. It must not introduce hosted behavior, MCP resources, persistence,
6 * search, vectors, summaries, PageIndex, OCR, LLM calls, provider routing,
7 * snippets, or note body output.
8 */
9 import { describe, it } from 'node:test';
10 import assert from 'node:assert/strict';
11 import fs from 'fs';
12 import path from 'path';
13 import { fileURLToPath } from 'url';
14 import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15 import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
16
17 import { readSectionSource } from '../lib/section-source-note.mjs';
18
19 const __dirname = path.dirname(fileURLToPath(import.meta.url));
20 const repoRoot = path.dirname(__dirname);
21 const fixtureVault = path.join(__dirname, 'fixtures', 'vault-fs');
22 process.env.KNOWTATION_VAULT_PATH = fixtureVault;
23
24 const { createKnowtationMcpServer } = await import('../mcp/create-server.mjs');
25
26 async function connectPair() {
27 process.env.KNOWTATION_VAULT_PATH = fixtureVault;
28 const mcpServer = createKnowtationMcpServer();
29 const client = new Client({ name: 'section-source-local', version: '0.0.1' });
30 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
31 await mcpServer.connect(serverTransport);
32 await client.connect(clientTransport);
33 return { client };
34 }
35
36 function parseToolResult(result) {
37 const text = result.content?.[0]?.text;
38 assert.equal(typeof text, 'string');
39 return JSON.parse(text);
40 }
41
42 function readRepoFile(relativePath) {
43 return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
44 }
45
46 function sectionSourceToolSource() {
47 const source = readRepoFile('mcp/create-server.mjs');
48 const start = source.indexOf("server.registerTool(\n 'get_section_source'");
49 const end = source.indexOf("server.registerTool(\n 'list_notes'", start);
50 assert.notEqual(start, -1);
51 assert.notEqual(end, -1);
52 return source.slice(start, end);
53 }
54
55 describe('MCP get_section_source', () => {
56 it('unit: returns only the SectionSource v0 body-free allowlist', async () => {
57 const { client } = await connectPair();
58 try {
59 const result = await client.callTool({
60 name: 'get_section_source',
61 arguments: { path: 'inbox/one.md' },
62 });
63 const data = parseToolResult(result);
64
65 assert.equal(result.isError, undefined);
66 assert.deepEqual(Object.keys(data), ['schema', 'path', 'title', 'sections', 'truncated']);
67 assert.deepEqual(Object.keys(data.sections[0]), [
68 'section_id',
69 'heading_id',
70 'level',
71 'heading_path',
72 'heading_text',
73 'child_section_ids',
74 'body_available',
75 'body_returned',
76 'snippet_returned',
77 ]);
78 assert.equal(data.schema, 'knowtation.section_source/v0');
79 assert.equal(data.sections[0].body_returned, false);
80 assert.equal(data.sections[0].snippet_returned, false);
81 } finally {
82 await client.close();
83 }
84 });
85
86 it('integration: matches readSectionSource for the same authorized note', async () => {
87 const { client } = await connectPair();
88 try {
89 const result = await client.callTool({
90 name: 'get_section_source',
91 arguments: { path: 'inbox/one.md' },
92 });
93 const data = parseToolResult(result);
94
95 assert.deepEqual(data, readSectionSource(fixtureVault, 'inbox/one.md'));
96 } finally {
97 await client.close();
98 }
99 });
100
101 it('end-to-end: returns section candidates without body, snippets, resources, or absolute paths', async () => {
102 const { client } = await connectPair();
103 try {
104 const result = await client.callTool({
105 name: 'get_section_source',
106 arguments: { path: 'inbox/one.md' },
107 });
108 const data = parseToolResult(result);
109 const serialized = JSON.stringify(data);
110
111 assert.equal(data.path, 'inbox/one.md');
112 assert.equal(data.title, 'one');
113 assert.deepEqual(data.sections, [
114 {
115 section_id: 'inbox-one-md:h1-inbox-one-0001',
116 heading_id: 'h1-inbox-one-0001',
117 level: 1,
118 heading_path: ['Inbox one'],
119 heading_text: 'Inbox one',
120 child_section_ids: [],
121 body_available: true,
122 body_returned: false,
123 snippet_returned: false,
124 },
125 ]);
126 assert.equal(Object.hasOwn(data, 'body'), false);
127 assert.equal(Object.hasOwn(data, 'frontmatter'), false);
128 assert.equal(Object.hasOwn(data, 'snippet'), false);
129 assert.equal(Object.hasOwn(data, 'summary'), false);
130 assert.equal(serialized.includes('Body of inbox one'), false);
131 assert.equal(serialized.includes('2025-03-01'), false);
132 assert.equal(serialized.includes('/Users/'), false);
133 assert.equal(serialized.includes('knowtation://'), false);
134 } finally {
135 await client.close();
136 }
137 });
138
139 it('stress: repeated MCP calls are deterministic and bounded', async () => {
140 const { client } = await connectPair();
141 try {
142 const started = Date.now();
143 const outputs = [];
144 for (let index = 0; index < 10; index += 1) {
145 const result = await client.callTool({
146 name: 'get_section_source',
147 arguments: { path: 'projects/foo/note.md' },
148 });
149 outputs.push(parseToolResult(result));
150 }
151 const elapsedMs = Date.now() - started;
152
153 for (const output of outputs) {
154 assert.deepEqual(output, outputs[0]);
155 assert.equal(output.sections.length, 1);
156 }
157 assert.ok(elapsedMs < 1000, `expected repeated MCP calls under 1000ms, got ${elapsedMs}ms`);
158 } finally {
159 await client.close();
160 }
161 });
162
163 it('data-integrity: MCP tool does not write notes, sidecars, indexes, vectors, or summaries', () => {
164 const implementation = [sectionSourceToolSource(), readRepoFile('lib/section-source-note.mjs')].join('\n');
165
166 assert.doesNotMatch(implementation, /\bwriteFile(Sync)?\s*\(/);
167 assert.doesNotMatch(implementation, /\bappendFile(Sync)?\s*\(/);
168 assert.doesNotMatch(implementation, /\bmkdir(Sync)?\s*\(/);
169 assert.doesNotMatch(implementation, /\bstoreMemory\s*\(/);
170 assert.doesNotMatch(implementation, /\brunIndex\s*\(/);
171 assert.doesNotMatch(implementation, /\bsidecar[A-Za-z0-9_]*\s*=/);
172 assert.doesNotMatch(implementation, /\bvector[A-Za-z0-9_]*\s*=/);
173 assert.doesNotMatch(implementation, /\bsummary[A-Za-z0-9_]*\s*=/);
174 });
175
176 it('performance: MCP tool stays one-note and provider-free', () => {
177 const toolSource = sectionSourceToolSource();
178
179 assert.match(toolSource, /readSectionSource\(config\.vault_path, args\.path\)/);
180 assert.doesNotMatch(toolSource, /\brunSearch\s*\(/);
181 assert.doesNotMatch(toolSource, /\brunKeywordSearch\s*\(/);
182 assert.doesNotMatch(toolSource, /\brunListNotes\s*\(/);
183 assert.doesNotMatch(toolSource, /\bfetch\s*\(/);
184 assert.doesNotMatch(toolSource, /\brerankWithSampling\s*\(/);
185 });
186
187 it('security: missing and traversal paths return MCP JSON errors without body data', async () => {
188 const { client } = await connectPair();
189 try {
190 for (const [requestedPath, errorPattern] of [
191 ['inbox/missing.md', /Note not found/],
192 ['../../../etc/passwd', /Invalid path|escape/],
193 ]) {
194 const result = await client.callTool({
195 name: 'get_section_source',
196 arguments: { path: requestedPath },
197 });
198 const data = parseToolResult(result);
199 const serialized = JSON.stringify(data);
200
201 assert.equal(result.isError, true);
202 assert.equal(data.code, 'RUNTIME_ERROR');
203 assert.match(data.error, errorPattern);
204 assert.equal(serialized.includes('Body of inbox one'), false);
205 assert.equal(serialized.includes('knowtation://'), false);
206 assert.equal(serialized.includes('/Users/'), false);
207 }
208 } finally {
209 await client.close();
210 }
211 });
212 });