register.mjs
754 lines 23.5 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Register Issue #1 Phase A MCP resources on an McpServer instance.
3 */
4
5 import fs from 'fs';
6 import path from 'path';
7 import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
8 import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
9 import { loadConfig } from '../../lib/config.mjs';
10 import { readNote, resolveVaultRelativePath, listMarkdownFiles } from '../../lib/vault.mjs';
11 import { buildVaultListing, listMediaFiles, listTemplateFiles } from './listing.mjs';
12 import { noteToMarkdown } from './note.mjs';
13 import {
14 buildIndexStats,
15 buildTagsResource,
16 buildProjectsResource,
17 redactConfig,
18 buildMemoryResource,
19 buildMemorySummaryResource,
20 buildMemoryEventsResource,
21 buildMemoryTypeResource,
22 buildMemoryIndexResource,
23 buildMemoryTopicResource,
24 buildAirLogResource,
25 } from './metadata.mjs';
26 import { buildKnowledgeGraph } from './graph.mjs';
27 import { extractImageUrls, extractVideoUrls } from '../../lib/media-url-extract.mjs';
28 import { fetchImageAsBase64 } from './image-fetch.mjs';
29 import { MCP_RESOURCE_PAGE_SIZE } from './pagination.mjs';
30
31 function jsonContent(uri, obj) {
32 return {
33 contents: [
34 {
35 uri: uri.toString(),
36 mimeType: 'application/json',
37 text: JSON.stringify(obj, null, 2),
38 },
39 ],
40 };
41 }
42
43 function textContent(uri, mimeType, text) {
44 return {
45 contents: [
46 {
47 uri: uri.toString(),
48 mimeType,
49 text,
50 },
51 ],
52 };
53 }
54
55 /**
56 * Fetch Muse commit-graph context for the ``knowtation://prime`` bootstrap.
57 *
58 * Phase 4.5 implementation: calls `muse log --json --max 100` as a subprocess
59 * to read recent commits, then extracts last_consolidation and hot_notes from
60 * the commit records' metadata fields written by Phase 4.2 (--event-type,
61 * --agent-id, --model-id) and structured_delta.
62 *
63 * Phase 5 upgrade: replace this function body with a JSON-RPC call to the
64 * `knowtation/prime-context` Muse MCP tool once it ships.
65 *
66 * Security: the subprocess is invoked with a fixed argument list — no user
67 * input or interpolated strings enter the command.
68 *
69 * @returns {Promise<object|null>} Prime context object or null on failure.
70 */
71 async function _fetchMusePrimeContext() {
72 const { execFile } = await import('child_process');
73 const { promisify } = await import('util');
74 const execFileAsync = promisify(execFile);
75
76 // Fetch recent commits as JSON. 'muse log --json --max 100' is a fixed
77 // command; no user input is interpolated.
78 let stdout;
79 try {
80 ({ stdout } = await execFileAsync('muse', ['log', '--json', '--max', '100'], {
81 timeout: 10_000, // 10 s timeout; non-blocking for the MCP caller
82 maxBuffer: 2 * 1024 * 1024, // 2 MiB — enough for 100 commits
83 }));
84 } catch (err) {
85 // muse CLI not available or repo not initialised; return null gracefully.
86 return null;
87 }
88
89 let commits;
90 try {
91 commits = JSON.parse(stdout);
92 if (!Array.isArray(commits)) return null;
93 } catch {
94 return null;
95 }
96
97 const CONSOLIDATION_KINDS = new Set(['consolidation', 'consolidation_pass']);
98 const noteEditCounts = new Map();
99 let lastConsolidation = null;
100
101 for (const commit of commits) {
102 const eventType = commit?.metadata?.event_type ?? null;
103
104 if (lastConsolidation === null && CONSOLIDATION_KINDS.has(eventType)) {
105 lastConsolidation = {
106 commit_id: commit.commit_id ?? '',
107 committed_at: commit.committed_at ?? '',
108 message: (commit.message ?? '').slice(0, 200),
109 agent_id: commit.agent_id ?? '',
110 model_id: commit.model_id ?? '',
111 };
112 }
113
114 // Accumulate note-path edit counts from structured_delta ops.
115 const ops = commit?.structured_delta?.ops ?? [];
116 for (const op of ops) {
117 const addr = op?.address ?? '';
118 let notePath = null;
119 if (typeof addr === 'string') {
120 if (addr.includes('::')) {
121 const [path] = addr.split('::', 2);
122 if (path.endsWith('.md')) notePath = path;
123 } else if (addr.endsWith('.md')) {
124 notePath = addr;
125 }
126 }
127 if (notePath) {
128 noteEditCounts.set(notePath, (noteEditCounts.get(notePath) ?? 0) + 1);
129 }
130 }
131 }
132
133 const hotNotes = [...noteEditCounts.entries()]
134 .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
135 .slice(0, 10)
136 .map(([path, edits]) => ({ path, edits }));
137
138 return {
139 schema_version: '1.0.0',
140 source: 'muse-commit-graph',
141 commits_scanned: commits.length,
142 last_consolidation: lastConsolidation,
143 hot_notes: hotNotes,
144 };
145 }
146
147 /**
148 * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
149 */
150 export function registerKnowtationResources(server) {
151 server.server.registerCapabilities({
152 resources: { subscribe: true },
153 });
154
155 server.registerResource(
156 'vault-root-listing',
157 'knowtation://vault/',
158 {
159 title: 'Vault listing (all notes)',
160 description: 'JSON list of notes under the vault (paginated, max 500 per request).',
161 },
162 async (uri) => {
163 const config = loadConfig();
164 return jsonContent(uri, buildVaultListing(config, ''));
165 }
166 );
167
168 server.registerResource(
169 'vault-inbox-listing',
170 'knowtation://vault/inbox',
171 {
172 title: 'Inbox listing',
173 description: 'JSON list of notes under vault/inbox/.',
174 },
175 async (uri) => {
176 const config = loadConfig();
177 return jsonContent(uri, buildVaultListing(config, 'inbox'));
178 }
179 );
180
181 server.registerResource(
182 'vault-captures-listing',
183 'knowtation://vault/captures',
184 {
185 title: 'Captures listing',
186 description: 'JSON list of notes under vault/captures/ (if present).',
187 },
188 async (uri) => {
189 const config = loadConfig();
190 return jsonContent(uri, buildVaultListing(config, 'captures'));
191 }
192 );
193
194 server.registerResource(
195 'vault-imports-listing',
196 'knowtation://vault/imports',
197 {
198 title: 'Imports listing',
199 description: 'JSON list of notes under vault/imports/ (if present).',
200 },
201 async (uri) => {
202 const config = loadConfig();
203 return jsonContent(uri, buildVaultListing(config, 'imports'));
204 }
205 );
206
207 server.registerResource(
208 'vault-media-audio',
209 'knowtation://vault/media/audio',
210 {
211 title: 'Audio media files',
212 description: 'JSON list of audio files under vault/media/audio/.',
213 },
214 async (uri) => {
215 const config = loadConfig();
216 return jsonContent(
217 uri,
218 listMediaFiles(config.vault_path, 'media/audio', ['.mp3', '.m4a', '.wav', '.ogg', '.flac', '.aac', '.webm'])
219 );
220 }
221 );
222
223 server.registerResource(
224 'vault-media-video',
225 'knowtation://vault/media/video',
226 {
227 title: 'Video media files',
228 description: 'JSON list of video files under vault/media/video/.',
229 },
230 async (uri) => {
231 const config = loadConfig();
232 return jsonContent(
233 uri,
234 listMediaFiles(config.vault_path, 'media/video', ['.mp4', '.mov', '.webm', '.mkv', '.avi', '.m4v'])
235 );
236 }
237 );
238
239 server.registerResource(
240 'vault-templates-index',
241 'knowtation://vault/templates',
242 {
243 title: 'Template paths',
244 description: 'List of markdown templates under vault/templates/.',
245 },
246 async (uri) => {
247 const config = loadConfig();
248 return jsonContent(uri, listTemplateFiles(config.vault_path));
249 }
250 );
251
252 server.registerResource(
253 'index-stats',
254 'knowtation://index/stats',
255 {
256 title: 'Index statistics',
257 description: 'Note count, chunk count in vector store, embedding config.',
258 },
259 async (uri) => {
260 const config = loadConfig();
261 const stats = await buildIndexStats(config);
262 return jsonContent(uri, stats);
263 }
264 );
265
266 server.registerResource(
267 'index-tags',
268 'knowtation://tags',
269 {
270 title: 'Tag facets',
271 description: 'All tags with counts and projects.',
272 },
273 async (uri) => {
274 const config = loadConfig();
275 return jsonContent(uri, buildTagsResource(config));
276 }
277 );
278
279 server.registerResource(
280 'index-projects',
281 'knowtation://projects',
282 {
283 title: 'Project manifest',
284 description: 'Projects inferred from notes with note counts.',
285 },
286 async (uri) => {
287 const config = loadConfig();
288 return jsonContent(uri, buildProjectsResource(config));
289 }
290 );
291
292 server.registerResource(
293 'config-snapshot',
294 'knowtation://config',
295 {
296 title: 'Redacted config',
297 description: 'Non-secret config snapshot for agents.',
298 },
299 async (uri) => {
300 const config = loadConfig();
301 return jsonContent(uri, redactConfig(config));
302 }
303 );
304
305 /**
306 * Bootstrap “prime” — small JSON for agents to readResource first (no vault bodies).
307 * Hosted equivalent: knowtation://hosted/prime (gateway MCP).
308 */
309 server.registerResource(
310 'prime-bootstrap',
311 'knowtation://prime',
312 {
313 title: 'MCP bootstrap (prime)',
314 description:
315 'Session-oriented hints: redacted config summary, suggested next resource URIs, and doc pointers. Pair with knowtation://config for full non-secret config.',
316 },
317 async (uri) => {
318 const config = loadConfig();
319 const snapshot = redactConfig(config);
320
321 // Phase 4.5 — Muse commit-graph context (opt-in feature flag).
322 // Set KNOWTATION_MUSE_ENABLED=true to populate muse_context.
323 // Phase 5 upgrade path: replace _fetchMusePrimeContext() with a call to
324 // the `knowtation/prime-context` Muse MCP tool once Phase 5 ships.
325 let museContext = null;
326 if (process.env.KNOWTATION_MUSE_ENABLED === 'true') {
327 museContext = await _fetchMusePrimeContext().catch((err) => {
328 // Non-fatal: muse_context is a best-effort enhancement.
329 console.error('[knowtation://prime] muse context unavailable:', err?.message ?? String(err));
330 return null;
331 });
332 }
333
334 const payload = {
335 schema: 'knowtation.prime/v1',
336 surface: 'self-hosted',
337 prime_uri: 'knowtation://prime',
338 config: snapshot,
339 // Commit-graph context from Muse (Phase 4.5).
340 // null unless KNOWTATION_MUSE_ENABLED=true and `muse` CLI is reachable.
341 muse_context: museContext,
342 suggested_next_resources: [
343 'knowtation://config',
344 'knowtation://vault/',
345 'knowtation://index/stats',
346 'knowtation://memory/',
347 ],
348 docs: {
349 why_knowtation: 'docs/TOKEN-SAVINGS.md',
350 agent_integration: 'docs/AGENT-INTEGRATION.md',
351 retrieval: 'docs/RETRIEVAL-AND-CLI-REFERENCE.md',
352 },
353 token_layers: {
354 vault_retrieval:
355 'Vault MCP/CLI retrieval (search, snippets, limits) is the primary in-product token saver.',
356 terminal_tooling:
357 'Shrinking terminal or shell logs is optional tooling on your coding host; Knowtation does not run canister-side shell hooks.',
358 },
359 };
360 return jsonContent(uri, payload);
361 }
362 );
363
364 server.registerResource(
365 'memory-last-search',
366 'knowtation://memory/last_search',
367 {
368 title: 'Last search (memory)',
369 description: 'Last stored search query and paths when memory.enabled.',
370 },
371 async (uri) => {
372 const config = loadConfig();
373 return jsonContent(uri, buildMemoryResource(config, 'last_search'));
374 }
375 );
376
377 server.registerResource(
378 'memory-last-export',
379 'knowtation://memory/last_export',
380 {
381 title: 'Last export (memory)',
382 description: 'Last export provenance when memory.enabled.',
383 },
384 async (uri) => {
385 const config = loadConfig();
386 return jsonContent(uri, buildMemoryResource(config, 'last_export'));
387 }
388 );
389
390 server.registerResource(
391 'memory-summary',
392 'knowtation://memory/',
393 {
394 title: 'Memory summary',
395 description: 'Memory layer status: enabled, provider, event counts, last activity.',
396 },
397 async (uri) => {
398 const config = loadConfig();
399 return jsonContent(uri, buildMemorySummaryResource(config));
400 }
401 );
402
403 server.registerResource(
404 'memory-events',
405 'knowtation://memory/events',
406 {
407 title: 'Recent memory events',
408 description: 'Last 50 memory events from the event log.',
409 },
410 async (uri) => {
411 const config = loadConfig();
412 return jsonContent(uri, buildMemoryEventsResource(config));
413 }
414 );
415
416 server.registerResource(
417 'memory-index',
418 'knowtation://memory/index',
419 {
420 title: 'Memory pointer index',
421 description: 'Lightweight markdown index (~150 chars/line) of memory state. Designed to be cheap enough for agents to always include in context. Lists event types with counts and latest summaries, plus recent activity.',
422 },
423 async (uri) => {
424 const config = loadConfig();
425 const result = buildMemoryIndexResource(config);
426 if (!result.enabled || !result.index) {
427 return jsonContent(uri, result);
428 }
429 return textContent(uri, 'text/markdown', result.index.markdown);
430 }
431 );
432
433 const memoryTopicTemplate = new ResourceTemplate('knowtation://memory/topic/{slug}', {
434 list: async () => {
435 try {
436 const config = loadConfig();
437 if (!config.memory?.enabled) return { resources: [] };
438 const { createMemoryManager } = await import('../../lib/memory.mjs');
439 const mm = createMemoryManager(config);
440 const topics = mm.listTopics();
441 return {
442 resources: topics.map((slug) => ({
443 uri: `knowtation://memory/topic/${slug}`,
444 name: slug,
445 mimeType: 'application/json',
446 description: `Memory events for topic: ${slug}`,
447 })),
448 };
449 } catch (_) {
450 return { resources: [] };
451 }
452 },
453 });
454
455 server.registerResource(
456 'memory-topic',
457 memoryTopicTemplate,
458 {
459 title: 'Memory topic partition',
460 description: 'Events partitioned by topic slug. Topics are derived from event data (path directory, query keywords, explicit data.topic).',
461 },
462 async (uri, variables) => {
463 const config = loadConfig();
464 let slug = variables.slug;
465 if (Array.isArray(slug)) slug = slug[0];
466 slug = decodeURIComponent(String(slug || ''));
467 if (!slug || slug.includes('..')) {
468 throw new McpError(ErrorCode.InvalidParams, 'Invalid topic slug');
469 }
470 return jsonContent(uri, buildMemoryTopicResource(config, slug));
471 }
472 );
473
474 server.registerResource(
475 'air-log',
476 'knowtation://air/log',
477 {
478 title: 'AIR attestation log',
479 description: 'Placeholder until AIR ids are persisted (see docs/MCP-RESOURCES-PHASE-A.md).',
480 },
481 async (uri) => {
482 return jsonContent(uri, buildAirLogResource());
483 }
484 );
485
486 server.registerResource(
487 'index-graph',
488 'knowtation://index/graph',
489 {
490 title: 'Knowledge graph',
491 description: 'Nodes (notes) and edges (wikilinks, follows, summarizes, causal_chain).',
492 },
493 async (uri) => {
494 const config = loadConfig();
495 return jsonContent(uri, buildKnowledgeGraph(config));
496 }
497 );
498
499 const templateNoteUri = new ResourceTemplate('knowtation://vault/templates/{+name}', {
500 list: async () => {
501 const config = loadConfig();
502 const { templates } = listTemplateFiles(config.vault_path);
503 const resources = templates.map((rel) => {
504 const name = rel.replace(/^templates\//, '');
505 const uri = `knowtation://vault/templates/${name}`;
506 return {
507 uri,
508 name: name.split('/').pop() || name,
509 mimeType: 'text/markdown',
510 description: `Template: ${name}`,
511 };
512 });
513 return { resources };
514 },
515 });
516
517 server.registerResource(
518 'vault-template-file',
519 templateNoteUri,
520 {
521 title: 'Vault template',
522 description: 'Markdown template under vault/templates/.',
523 },
524 async (uri, variables) => {
525 const config = loadConfig();
526 let name = variables.name;
527 if (Array.isArray(name)) name = name[0];
528 name = decodeURIComponent(String(name || '').replace(/\\/g, '/'));
529 if (!name || name.includes('..')) {
530 throw new McpError(ErrorCode.InvalidParams, 'Invalid template name');
531 }
532 let rel = `templates/${name}`;
533 if (!rel.endsWith('.md')) rel = `${rel}.md`;
534 const full = path.join(config.vault_path, rel);
535 if (!full.startsWith(path.resolve(config.vault_path)) || !fs.existsSync(full) || !fs.statSync(full).isFile()) {
536 throw new McpError(ErrorCode.InvalidParams, `Template not found: ${name}`);
537 }
538 const body = fs.readFileSync(full, 'utf8');
539 return textContent(uri, 'text/markdown', body);
540 }
541 );
542
543 const vaultPathTemplate = new ResourceTemplate('knowtation://vault/{+path}', {
544 list: async () => {
545 const config = loadConfig();
546 const { listMarkdownFiles } = await import('../../lib/vault.mjs');
547 const paths = listMarkdownFiles(config.vault_path, { ignore: config.ignore });
548 const resources = paths.slice(0, 500).map((p) => {
549 const u = `knowtation://vault/${p}`;
550 let title = p.split('/').pop() || p;
551 let description = '';
552 try {
553 const n = readNote(config.vault_path, p);
554 title = n.frontmatter?.title || title;
555 description = (n.body || '').slice(0, 160).replace(/\s+/g, ' ').trim();
556 } catch (_) {}
557 return {
558 uri: u,
559 name: title,
560 mimeType: 'text/markdown',
561 description: description || undefined,
562 };
563 });
564 return { resources };
565 },
566 });
567
568 server.registerResource(
569 'vault-path',
570 vaultPathTemplate,
571 {
572 title: 'Vault note or listing',
573 description: 'Markdown note if path ends with .md; otherwise JSON listing for that folder prefix.',
574 },
575 async (uri, variables) => {
576 const config = loadConfig();
577 let rel = variables.path;
578 if (Array.isArray(rel)) rel = rel[0];
579 rel = decodeURIComponent(String(rel || '').replace(/\\/g, '/'));
580 if (rel.includes('..')) {
581 throw new McpError(ErrorCode.InvalidParams, 'Invalid path');
582 }
583
584 if (rel.endsWith('.md')) {
585 resolveVaultRelativePath(config.vault_path, rel);
586 const note = readNote(config.vault_path, rel);
587 const title = note.frontmatter?.title || rel.split('/').pop();
588 const desc = (note.body || '').slice(0, 160).replace(/\s+/g, ' ').trim();
589 return {
590 contents: [
591 {
592 uri: uri.toString(),
593 mimeType: 'text/markdown',
594 text: noteToMarkdown(note),
595 _meta: { title, description: desc },
596 },
597 ],
598 };
599 }
600
601 return jsonContent(uri, buildVaultListing(config, rel));
602 }
603 );
604
605 // --- Phase 18A: MCP Image Resources ---
606
607 const noteImageTemplate = new ResourceTemplate('knowtation://vault/{+notePath}/image/{index}', {
608 list: async () => {
609 const config = loadConfig();
610 const paths = listMarkdownFiles(config.vault_path, { ignore: config.ignore });
611 const resources = [];
612 for (const p of paths.slice(0, MCP_RESOURCE_PAGE_SIZE)) {
613 try {
614 const note = readNote(config.vault_path, p);
615 const images = extractImageUrls(note.body);
616 for (let i = 0; i < images.length; i++) {
617 const img = images[i];
618 const name = img.alt || img.url.split('/').pop().split('?')[0] || `image-${i}`;
619 resources.push({
620 uri: `knowtation://vault/${p}/image/${i}`,
621 name,
622 mimeType: img.mimeType,
623 description: `Image in ${p}`,
624 });
625 }
626 } catch (_) {}
627 if (resources.length >= MCP_RESOURCE_PAGE_SIZE) break;
628 }
629 return { resources: resources.slice(0, MCP_RESOURCE_PAGE_SIZE) };
630 },
631 });
632
633 server.registerResource(
634 'note-image',
635 noteImageTemplate,
636 {
637 title: 'Note embedded image',
638 description: 'Image referenced in a note body via ![alt](url). Returns base64 blob with typed mimeType for vision-capable MCP clients.',
639 },
640 async (uri, variables) => {
641 const config = loadConfig();
642 let notePath = variables.notePath;
643 if (Array.isArray(notePath)) notePath = notePath[0];
644 notePath = decodeURIComponent(String(notePath || '').replace(/\\/g, '/'));
645 if (notePath.includes('..')) {
646 throw new McpError(ErrorCode.InvalidParams, 'Invalid note path');
647 }
648
649 let idx = variables.index;
650 if (Array.isArray(idx)) idx = idx[0];
651 idx = parseInt(String(idx), 10);
652 if (isNaN(idx) || idx < 0) {
653 throw new McpError(ErrorCode.InvalidParams, 'Invalid image index');
654 }
655
656 resolveVaultRelativePath(config.vault_path, notePath);
657 const note = readNote(config.vault_path, notePath);
658 const images = extractImageUrls(note.body);
659 if (idx >= images.length) {
660 throw new McpError(ErrorCode.InvalidParams, `Image index ${idx} out of range (note has ${images.length} images)`);
661 }
662
663 const img = images[idx];
664 try {
665 const result = await fetchImageAsBase64(img.url);
666 return {
667 contents: [
668 {
669 uri: uri.toString(),
670 mimeType: result.mimeType,
671 blob: result.blob,
672 },
673 ],
674 };
675 } catch (e) {
676 throw new McpError(
677 ErrorCode.InternalError,
678 `Failed to fetch image from ${img.url}: ${e.message || e}`,
679 );
680 }
681 }
682 );
683
684 // --- Phase 18B: MCP Video Resources ---
685
686 const noteVideoTemplate = new ResourceTemplate('knowtation://vault/{+notePath}/video/{index}', {
687 list: async () => {
688 const config = loadConfig();
689 const paths = listMarkdownFiles(config.vault_path, { ignore: config.ignore });
690 const resources = [];
691 for (const p of paths.slice(0, MCP_RESOURCE_PAGE_SIZE)) {
692 try {
693 const note = readNote(config.vault_path, p);
694 const videos = extractVideoUrls(note.body);
695 for (let i = 0; i < videos.length; i++) {
696 const vid = videos[i];
697 const name = vid.url.split('/').pop().split('?')[0] || `video-${i}`;
698 resources.push({
699 uri: `knowtation://vault/${p}/video/${i}`,
700 name,
701 mimeType: vid.mimeType,
702 description: `Video in ${p}`,
703 });
704 }
705 } catch (_) {}
706 if (resources.length >= MCP_RESOURCE_PAGE_SIZE) break;
707 }
708 return { resources: resources.slice(0, MCP_RESOURCE_PAGE_SIZE) };
709 },
710 });
711
712 server.registerResource(
713 'note-video',
714 noteVideoTemplate,
715 {
716 title: 'Note embedded video',
717 description: 'Video URL referenced in a note body. Returns the URL as text with typed video/* mimeType for video-capable agents.',
718 },
719 async (uri, variables) => {
720 const config = loadConfig();
721 let notePath = variables.notePath;
722 if (Array.isArray(notePath)) notePath = notePath[0];
723 notePath = decodeURIComponent(String(notePath || '').replace(/\\/g, '/'));
724 if (notePath.includes('..')) {
725 throw new McpError(ErrorCode.InvalidParams, 'Invalid note path');
726 }
727
728 let idx = variables.index;
729 if (Array.isArray(idx)) idx = idx[0];
730 idx = parseInt(String(idx), 10);
731 if (isNaN(idx) || idx < 0) {
732 throw new McpError(ErrorCode.InvalidParams, 'Invalid video index');
733 }
734
735 resolveVaultRelativePath(config.vault_path, notePath);
736 const note = readNote(config.vault_path, notePath);
737 const videos = extractVideoUrls(note.body);
738 if (idx >= videos.length) {
739 throw new McpError(ErrorCode.InvalidParams, `Video index ${idx} out of range (note has ${videos.length} videos)`);
740 }
741
742 const vid = videos[idx];
743 return {
744 contents: [
745 {
746 uri: uri.toString(),
747 mimeType: vid.mimeType,
748 text: vid.url,
749 },
750 ],
751 };
752 }
753 );
754 }
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 2 days ago