oscar64-mcp-docs 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stdio.js ADDED
@@ -0,0 +1,1260 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/server.ts
4
+ import { MCPServer } from "@mastra/mcp";
5
+
6
+ // src/indexing/tutorials.ts
7
+ import fs2 from "fs/promises";
8
+ import path3 from "path";
9
+
10
+ // src/config/tutorial-classification.ts
11
+ var DEFAULT_TUTORIAL_CATEGORY = "general";
12
+ var CONCEPT_RULES = [
13
+ {
14
+ concept: "timing_interrupts",
15
+ tests: [
16
+ { pattern: "#include\\s*<c64/rasterirq\\.h>", flags: "i", reason: "includes rasterirq api", weight: 3 },
17
+ { pattern: "\\brirq_[a-z0-9_]+\\b", flags: "i", reason: "uses rirq_* routines", weight: 2 },
18
+ { pattern: "\\birq\\b", flags: "i", reason: "mentions irq", weight: 1 }
19
+ ]
20
+ },
21
+ {
22
+ concept: "graphics_2d",
23
+ tests: [
24
+ { pattern: "#include\\s*<c64/(sprites|vic)\\.h>", flags: "i", reason: "uses c64 graphics headers", weight: 2 },
25
+ { pattern: "\\b(sprite|bitmap|tile|scroll|parallax|colorram)\\b", flags: "i", reason: "mentions graphics primitives", weight: 1 },
26
+ { pattern: "\\.(mcimg|spd|ctm)\\b", flags: "i", reason: "references graphics assets", weight: 1 }
27
+ ]
28
+ },
29
+ {
30
+ concept: "animation",
31
+ tests: [
32
+ { pattern: "\\b(animate|anim|moving|bounce|reflect|scroll)\\b", flags: "i", reason: "mentions motion behavior", weight: 1 },
33
+ { pattern: "\\bvspr_[a-z0-9_]+\\b", flags: "i", reason: "uses virtual sprite animation routines", weight: 2 }
34
+ ]
35
+ },
36
+ {
37
+ concept: "audio",
38
+ tests: [
39
+ { pattern: "#include\\s*<c64/sid\\.h>", flags: "i", reason: "includes sid api", weight: 3 },
40
+ { pattern: "\\.sid\\b", flags: "i", reason: "references sid asset", weight: 2 },
41
+ { pattern: "\\b(sid|music|audio|sound)\\b", flags: "i", reason: "mentions audio concepts", weight: 1 }
42
+ ]
43
+ },
44
+ {
45
+ concept: "memory_management",
46
+ tests: [
47
+ { pattern: "#include\\s*<c64/memmap\\.h>", flags: "i", reason: "includes memory map api", weight: 3 },
48
+ { pattern: "\\bmmap_[a-z0-9_]+\\b", flags: "i", reason: "uses mmap_* routines", weight: 2 },
49
+ { pattern: "\\b(bank|overlay|layout|resource region|full memory)\\b", flags: "i", reason: "mentions memory layout concepts", weight: 1 }
50
+ ]
51
+ },
52
+ {
53
+ concept: "math_numerics",
54
+ tests: [
55
+ { pattern: "#include\\s*<math\\.h>", flags: "i", reason: "includes math api", weight: 3 },
56
+ { pattern: "\\b(cordic|atan|cosin|sqrt|float|fixpoint|distance)\\b", flags: "i", reason: "mentions numeric methods", weight: 1 }
57
+ ]
58
+ },
59
+ {
60
+ concept: "data_structures_game_logic",
61
+ tests: [
62
+ { pattern: "\\badventure\\b", flags: "i", reason: "mentions adventure gameplay", weight: 2 },
63
+ { pattern: "\\b(token|parse|map|items|doors|keys)\\b", flags: "i", reason: "mentions game logic structures", weight: 1 }
64
+ ]
65
+ },
66
+ {
67
+ concept: "resource_pipeline",
68
+ tests: [
69
+ { pattern: "#embed", flags: "i", reason: "uses #embed resource pipeline", weight: 2 },
70
+ { pattern: "\\.\\./resources/", flags: "i", reason: "references shared resources", weight: 1 },
71
+ { pattern: "\\.(ctm|spd|sid|bin|mcimg)\\b", flags: "i", reason: "references packed assets", weight: 1 }
72
+ ]
73
+ },
74
+ {
75
+ concept: "build_tooling",
76
+ tests: [
77
+ { pattern: "\\bmake\\.bat\\b", flags: "i", reason: "includes build batch file", weight: 1 },
78
+ { pattern: "\\b(makefile|build\\.sh|compiler flag|oscar64)\\b", flags: "i", reason: "mentions build tooling", weight: 1 }
79
+ ]
80
+ },
81
+ {
82
+ concept: "io_storage",
83
+ tests: [
84
+ { pattern: "#include\\s*<c64/(kernalio|iecbus)\\.h>", flags: "i", reason: "includes storage/io api", weight: 3 },
85
+ { pattern: "\\b(file|disk|load|save|read|write|io)\\b", flags: "i", reason: "mentions io/storage operations", weight: 1 }
86
+ ]
87
+ },
88
+ {
89
+ concept: "input",
90
+ tests: [
91
+ { pattern: "#include\\s*<c64/(keyboard|joystick|mouse)\\.h>", flags: "i", reason: "includes input api", weight: 3 },
92
+ { pattern: "\\b(key|keyboard|joystick|mouse|cursor)\\b", flags: "i", reason: "mentions input handling", weight: 1 }
93
+ ]
94
+ }
95
+ ];
96
+ var CATEGORY_FROM_CONCEPTS = [
97
+ { category: "visuals", anyOf: ["graphics_2d", "animation"] },
98
+ { category: "systems", anyOf: ["timing_interrupts", "memory_management", "resource_pipeline", "build_tooling"] },
99
+ { category: "logic", anyOf: ["data_structures_game_logic", "math_numerics"] },
100
+ { category: "platform_io", anyOf: ["io_storage", "input", "audio"] }
101
+ ];
102
+
103
+ // src/search/minisearch.ts
104
+ import MiniSearch from "minisearch";
105
+
106
+ // src/utils.ts
107
+ import crypto from "crypto";
108
+ import fs from "fs/promises";
109
+ import path2 from "path";
110
+
111
+ // src/config.ts
112
+ import os from "os";
113
+ import path from "path";
114
+ var CACHE_DIR = process.env.OSCAR_MCP_CACHE_DIR ?? path.join(os.homedir(), ".cache", "oscar-mcp");
115
+ var META_DIR = path.join(CACHE_DIR, "meta");
116
+ var SOURCES_DIR = path.join(CACHE_DIR, "sources");
117
+ var INDEX_DIR = path.join(CACHE_DIR, "index");
118
+ var UPDATE_TTL_MS = 24 * 60 * 60 * 1e3;
119
+ var MAX_TEXT_BYTES = 1e6;
120
+ var GITHUB_API = "https://api.github.com";
121
+ var REPOS = [
122
+ { key: "oscar64", owner: "drmortalwombat", repo: "oscar64", trackingRef: process.env.OSCAR64_REF ?? "main" },
123
+ {
124
+ key: "OscarTutorials",
125
+ owner: "drmortalwombat",
126
+ repo: "OscarTutorials",
127
+ trackingRef: process.env.OSCAR_TUTORIALS_REF ?? "main"
128
+ }
129
+ ];
130
+ var TEXT_EXTS = /* @__PURE__ */ new Set([
131
+ ".md",
132
+ ".txt",
133
+ ".c",
134
+ ".h",
135
+ ".cpp",
136
+ ".s",
137
+ ".asm",
138
+ ".inc",
139
+ ".json",
140
+ ".yml",
141
+ ".yaml",
142
+ ".bat"
143
+ ]);
144
+
145
+ // src/utils.ts
146
+ function slugify(value) {
147
+ return value.trim().toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
148
+ }
149
+ function makeExcerpt(text, maxLen = 420) {
150
+ const compact = text.replace(/\s+/g, " ").trim();
151
+ if (compact.length <= maxLen) return compact;
152
+ return `${compact.slice(0, maxLen - 1)}\u2026`;
153
+ }
154
+ function sha256(buf) {
155
+ return crypto.createHash("sha256").update(buf).digest("hex");
156
+ }
157
+ async function readJson(filePath) {
158
+ try {
159
+ const raw = await fs.readFile(filePath, "utf8");
160
+ return JSON.parse(raw);
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ async function writeJson(filePath, value) {
166
+ await fs.mkdir(path2.dirname(filePath), { recursive: true });
167
+ await fs.writeFile(filePath, JSON.stringify(value, null, 2), "utf8");
168
+ }
169
+ async function exists(filePath) {
170
+ try {
171
+ await fs.stat(filePath);
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ function safeJoin(root, relPath) {
178
+ const out = path2.resolve(root, relPath);
179
+ const rootResolved = path2.resolve(root);
180
+ if (!out.startsWith(rootResolved + path2.sep) && out !== rootResolved) {
181
+ throw new Error("Path traversal blocked");
182
+ }
183
+ return out;
184
+ }
185
+ function isTextPath(filePath) {
186
+ return TEXT_EXTS.has(path2.extname(filePath).toLowerCase());
187
+ }
188
+ async function safeReadText(filePath) {
189
+ const stat = await fs.stat(filePath);
190
+ if (!stat.isFile()) throw new Error("Not a file");
191
+ if (stat.size > MAX_TEXT_BYTES) throw new Error("File too large");
192
+ const buf = await fs.readFile(filePath);
193
+ return buf.toString("utf8");
194
+ }
195
+ async function listFilesRecursive(root) {
196
+ const out = [];
197
+ async function walk(dir) {
198
+ const entries = await fs.readdir(dir, { withFileTypes: true });
199
+ for (const e of entries) {
200
+ const abs = path2.join(dir, e.name);
201
+ if (e.isDirectory()) await walk(abs);
202
+ else if (e.isFile()) out.push(abs);
203
+ }
204
+ }
205
+ await walk(root);
206
+ return out;
207
+ }
208
+
209
+ // src/search/minisearch.ts
210
+ function oscarTokenizer(text) {
211
+ return text.toLowerCase().split(/[^0-9a-zA-Z_\-$]+/).filter(Boolean);
212
+ }
213
+ function oscarProcessTerm(term) {
214
+ const t = term.toLowerCase();
215
+ if (t.startsWith("-") && t.length > 2) return [t, t.slice(1)];
216
+ if (t.startsWith("$") && t.length > 2) {
217
+ const hex = t.slice(1);
218
+ return [t, hex, `0x${hex}`];
219
+ }
220
+ return [t];
221
+ }
222
+ function inferCombineMode(query) {
223
+ return query.trim().split(/\s+/).filter(Boolean).length <= 1 ? "OR" : "AND";
224
+ }
225
+ function expandTaskQuery(task) {
226
+ const synonyms = {
227
+ irq: ["interrupt", "raster", "nmi"],
228
+ sprite: ["sprites", "multiplex", "mux"],
229
+ scroll: ["scrolling", "scroller"],
230
+ sid: ["music", "audio", "sound"],
231
+ vic: ["vic-ii", "video", "raster"],
232
+ border: ["$d020", "d020"]
233
+ };
234
+ const lower = task.toLowerCase();
235
+ const terms = /* @__PURE__ */ new Set([task]);
236
+ for (const [base, syns] of Object.entries(synonyms)) {
237
+ if (lower.includes(base) || syns.some((s) => lower.includes(s))) {
238
+ terms.add(base);
239
+ for (const s of syns) terms.add(s);
240
+ }
241
+ }
242
+ return [...terms].join(" ");
243
+ }
244
+ function buildDocsSearch(state) {
245
+ const ms = new MiniSearch({
246
+ fields: ["title", "keywords", "body"],
247
+ storeFields: ["kind", "title", "uri", "excerpt", "repo", "ref", "anchor", "tutorialId"],
248
+ tokenize: oscarTokenizer,
249
+ processTerm: oscarProcessTerm,
250
+ searchOptions: {
251
+ boost: { title: 6, keywords: 3, body: 1 },
252
+ prefix: true,
253
+ fuzzy: 0.12,
254
+ combineWith: "AND"
255
+ }
256
+ });
257
+ const docs = [];
258
+ for (const s of state.manualSections) {
259
+ const body = state.manualLines.slice(s.startLine, s.endLine + 1).join("\n");
260
+ docs.push({
261
+ id: `manual:${s.anchor}`,
262
+ kind: "manual_section",
263
+ title: s.heading,
264
+ uri: `docs://oscar64/manual#${s.anchor}`,
265
+ excerpt: makeExcerpt(body),
266
+ body,
267
+ anchor: s.anchor,
268
+ repo: "oscar64",
269
+ ref: state.repos.oscar64.resolvedSha
270
+ });
271
+ }
272
+ for (const t of state.tutorials) {
273
+ const keywords = t.keywords.join(" ");
274
+ const body = `${t.title}
275
+ ${t.files.join("\n")}
276
+ ${t.previewText}`;
277
+ docs.push({
278
+ id: `tutorial:${t.id}`,
279
+ kind: "tutorial",
280
+ title: `${t.id} ${t.title}`,
281
+ uri: `tutorial://${t.id}/manifest`,
282
+ excerpt: makeExcerpt(t.previewText || `${t.id} ${t.title}`),
283
+ body,
284
+ tutorialId: t.id,
285
+ keywords,
286
+ repo: "OscarTutorials",
287
+ ref: state.repos.OscarTutorials.resolvedSha
288
+ });
289
+ }
290
+ ms.addAll(docs);
291
+ return ms;
292
+ }
293
+
294
+ // src/indexing/tutorials.ts
295
+ function parseTutorialIdentity(folderName) {
296
+ const numeric = folderName.match(/^(\d+)\s+(.+)$/);
297
+ if (numeric) return { id: numeric[1], title: numeric[2].trim() };
298
+ return { id: slugify(folderName), title: folderName };
299
+ }
300
+ async function readFirstLines(filePath, maxLines = 120) {
301
+ const text = await safeReadText(filePath);
302
+ return text.split(/\r?\n/).slice(0, maxLines).join("\n");
303
+ }
304
+ function pickPrimaryFile(files) {
305
+ const normalized = files.map((f) => f.replace(/\\/g, "/"));
306
+ const readme = normalized.find((f) => /^readme(\.[^/]+)?$/i.test(path3.basename(f)));
307
+ if (readme) return readme;
308
+ const mainC = normalized.find((f) => f.toLowerCase() === "main.c") ?? normalized.find((f) => f.toLowerCase().endsWith("/main.c"));
309
+ if (mainC) return mainC;
310
+ const c = normalized.filter((f) => f.toLowerCase().endsWith(".c")).sort()[0];
311
+ if (c) return c;
312
+ const cpp = normalized.filter((f) => f.toLowerCase().endsWith(".cpp")).sort()[0];
313
+ if (cpp) return cpp;
314
+ return normalized.sort()[0] ?? null;
315
+ }
316
+ function extractAssetRefs(preview) {
317
+ const refs = /* @__PURE__ */ new Set();
318
+ const regex = /\.\.\/Resources\/[^"'\s)]+/g;
319
+ let match;
320
+ while ((match = regex.exec(preview)) !== null) refs.add(match[0]);
321
+ return [...refs];
322
+ }
323
+ function classifyConcepts(input) {
324
+ const haystack = [input.title, ...input.files, input.previewText, ...input.assetRefs].join("\n").toLowerCase();
325
+ const evidence = /* @__PURE__ */ new Map();
326
+ const scores = /* @__PURE__ */ new Map();
327
+ for (const rule of CONCEPT_RULES) {
328
+ for (const test of rule.tests) {
329
+ const re = new RegExp(test.pattern, test.flags ?? "i");
330
+ if (!re.test(haystack)) continue;
331
+ if (!evidence.has(rule.concept)) evidence.set(rule.concept, /* @__PURE__ */ new Set());
332
+ evidence.get(rule.concept).add(test.reason);
333
+ scores.set(rule.concept, (scores.get(rule.concept) ?? 0) + (test.weight ?? 1));
334
+ }
335
+ }
336
+ const conceptEvidence = {};
337
+ const conceptScores = {};
338
+ for (const [concept, reasons] of evidence.entries()) {
339
+ conceptEvidence[concept] = [...reasons].sort();
340
+ conceptScores[concept] = scores.get(concept) ?? 0;
341
+ }
342
+ const concepts = Object.keys(conceptEvidence).sort((a, b) => {
343
+ const scoreDiff = (conceptScores[b] ?? 0) - (conceptScores[a] ?? 0);
344
+ if (scoreDiff !== 0) return scoreDiff;
345
+ return a.localeCompare(b);
346
+ });
347
+ return { concepts, conceptEvidence, conceptScores };
348
+ }
349
+ function deriveCategoryFromConcepts(concepts) {
350
+ const set = new Set(concepts);
351
+ for (const group of CATEGORY_FROM_CONCEPTS) {
352
+ if (group.anyOf.some((concept) => set.has(concept))) return group.category;
353
+ }
354
+ return DEFAULT_TUTORIAL_CATEGORY;
355
+ }
356
+ function tutorialManifest(t) {
357
+ return {
358
+ id: t.id,
359
+ title: t.title,
360
+ slug: t.slug,
361
+ folder_name: t.folderName,
362
+ category: t.category,
363
+ primary_file: t.primaryFile,
364
+ files: t.files,
365
+ preview_text: t.previewText,
366
+ keywords: t.keywords,
367
+ asset_refs: t.assetRefs,
368
+ concepts: t.concepts,
369
+ concept_evidence: t.conceptEvidence
370
+ };
371
+ }
372
+ async function buildTutorialsIndex(tutorialsRoot) {
373
+ const entries = await fs2.readdir(tutorialsRoot, { withFileTypes: true });
374
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name.toLowerCase() !== "resources").sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
375
+ const tutorials = [];
376
+ for (const folderName of dirs) {
377
+ const absDir = path3.join(tutorialsRoot, folderName);
378
+ const filesAbs = await listFilesRecursive(absDir);
379
+ const files = filesAbs.map((f) => path3.relative(absDir, f));
380
+ if (files.length === 0) continue;
381
+ const { id, title } = parseTutorialIdentity(folderName);
382
+ const primaryFile = pickPrimaryFile(files);
383
+ let previewText = "";
384
+ if (primaryFile) {
385
+ try {
386
+ previewText = await readFirstLines(path3.join(absDir, primaryFile));
387
+ } catch {
388
+ previewText = "";
389
+ }
390
+ }
391
+ const assetRefs = extractAssetRefs(previewText);
392
+ const { concepts, conceptEvidence } = classifyConcepts({
393
+ title,
394
+ files,
395
+ previewText,
396
+ assetRefs
397
+ });
398
+ const keywords = /* @__PURE__ */ new Set();
399
+ for (const token of oscarTokenizer(`${title} ${folderName}`)) {
400
+ for (const term of oscarProcessTerm(token)) if (term.length >= 3) keywords.add(term);
401
+ }
402
+ for (const rel of files) {
403
+ for (const token of oscarTokenizer(path3.basename(rel, path3.extname(rel)))) {
404
+ for (const term of oscarProcessTerm(token)) if (term.length >= 3) keywords.add(term);
405
+ }
406
+ }
407
+ for (const token of oscarTokenizer(previewText)) {
408
+ for (const term of oscarProcessTerm(token)) if (term.length >= 3) keywords.add(term);
409
+ }
410
+ for (const concept of concepts) {
411
+ for (const token of oscarTokenizer(concept)) {
412
+ for (const term of oscarProcessTerm(token)) if (term.length >= 3) keywords.add(term);
413
+ }
414
+ }
415
+ tutorials.push({
416
+ id,
417
+ title,
418
+ slug: `${id}-${slugify(title)}`,
419
+ folderName,
420
+ absDir,
421
+ category: deriveCategoryFromConcepts(concepts),
422
+ primaryFile,
423
+ files,
424
+ previewText: makeExcerpt(previewText, 2e3),
425
+ keywords: [...keywords].slice(0, 220),
426
+ assetRefs,
427
+ concepts,
428
+ conceptEvidence
429
+ });
430
+ }
431
+ return tutorials;
432
+ }
433
+
434
+ // src/mcp/resources.ts
435
+ function indexContracts() {
436
+ return JSON.stringify(
437
+ {
438
+ tool_response_contract: {
439
+ ok: "boolean, true when tool call succeeded",
440
+ data: "object, present on ok=true",
441
+ error: {
442
+ code: "INVALID_INPUT|NOT_FOUND|UNSUPPORTED_SCOPE|OFFLINE|INTERNAL_ERROR",
443
+ message: "human-readable failure summary",
444
+ hint: "specific recovery action",
445
+ recoverable: "boolean",
446
+ suggested_tool_calls: "optional list of {tool,args}"
447
+ },
448
+ next_actions: "optional list of recommended follow-up actions",
449
+ meta: {
450
+ tool: "tool id",
451
+ generated_at: "ISO date-time",
452
+ contract_version: "string"
453
+ }
454
+ },
455
+ tool_intent_map: {
456
+ search_docs: "Start here for conceptual search over docs/tutorial manifests.",
457
+ get_doc_section: "Fetch authoritative manual section after search_docs.",
458
+ search_code: "Find concrete source snippets and line ranges.",
459
+ get_best_tutorial: "Route task to best tutorial with concept evidence.",
460
+ refresh_sources: "Refresh synced source snapshots.",
461
+ get_source_status: "Check source readiness and index sizes."
462
+ }
463
+ },
464
+ null,
465
+ 2
466
+ );
467
+ }
468
+ function indexToolingGuide() {
469
+ return [
470
+ "# Tooling Guide",
471
+ "",
472
+ "1. Understand feature intent: `search_docs`",
473
+ "2. Read authoritative manual: `get_doc_section`",
474
+ "3. Inspect practical code: `get_best_tutorial` then `search_code`",
475
+ "",
476
+ "If a tool returns `ok=false`, use `error.hint` and `suggested_tool_calls`."
477
+ ].join("\n");
478
+ }
479
+ function indexQuickstart() {
480
+ return [
481
+ "# Quickstart",
482
+ "",
483
+ "Workflow A: Find manual guidance",
484
+ "- Call `search_docs` with scope=manual",
485
+ "- Call `get_doc_section` using a returned anchor",
486
+ "",
487
+ "Workflow B: Find matching tutorial",
488
+ "- Call `get_best_tutorial` with task",
489
+ "- Read `tutorial://<id>/manifest`",
490
+ "- Read `tutorial://<id>/file/<path>` for key files",
491
+ "",
492
+ "Workflow C: Keep sources current",
493
+ "- Call `refresh_sources` when you need updated remote docs/examples",
494
+ "- Call `get_source_status` to verify source readiness"
495
+ ].join("\n");
496
+ }
497
+ function indexErrors() {
498
+ return JSON.stringify(
499
+ {
500
+ INVALID_INPUT: "Input schema/semantics invalid; fix arguments and retry.",
501
+ NOT_FOUND: "Requested section/tutorial/path not found; run search tools first.",
502
+ UNSUPPORTED_SCOPE: "Scope not supported by selected tool.",
503
+ OFFLINE: "Remote source status unavailable; retry later.",
504
+ INTERNAL_ERROR: "Unexpected server fault; retry and report if persistent."
505
+ },
506
+ null,
507
+ 2
508
+ );
509
+ }
510
+ function makeResources(getState) {
511
+ const resourceTemplates = async () => [
512
+ {
513
+ uriTemplate: "docs://oscar64/manual#{anchor}",
514
+ name: "Oscar64 Manual Section",
515
+ description: "A specific manual section by stable anchor.",
516
+ mimeType: "text/markdown"
517
+ },
518
+ {
519
+ uriTemplate: "tutorial://{id}/file/{path}",
520
+ name: "OscarTutorials file",
521
+ description: "A specific tutorial source file.",
522
+ mimeType: "text/plain"
523
+ },
524
+ {
525
+ uriTemplate: "repo://oscar64/{path}",
526
+ name: "oscar64 file",
527
+ description: "A specific text file from oscar64 repository snapshot.",
528
+ mimeType: "text/plain"
529
+ },
530
+ {
531
+ uriTemplate: "repo://OscarTutorials/{path}",
532
+ name: "OscarTutorials file",
533
+ description: "A specific text file from OscarTutorials repository snapshot.",
534
+ mimeType: "text/plain"
535
+ }
536
+ ];
537
+ const listResources = async () => {
538
+ const state = getState();
539
+ return [
540
+ { uri: "index://quickstart", name: "Quickstart", description: "Minimal tool call workflows.", mimeType: "text/markdown" },
541
+ { uri: "index://tooling-guide", name: "Tooling Guide", description: "Tool decision tree for coding agents.", mimeType: "text/markdown" },
542
+ { uri: "index://contracts", name: "Tool Contracts", description: "Standard input/output contract reference.", mimeType: "application/json" },
543
+ { uri: "index://errors", name: "Error Codes", description: "Error taxonomy and recovery guidance.", mimeType: "application/json" },
544
+ { uri: "index://status", name: "Source And Index Status", description: "Source readiness and index sizes", mimeType: "application/json" },
545
+ { uri: "index://manual-anchors", name: "Manual Anchors", description: "Headings from oscar64 manual", mimeType: "application/json" },
546
+ { uri: "index://tutorials", name: "Tutorial Index", description: "Normalized OscarTutorials catalog", mimeType: "application/json" },
547
+ {
548
+ uri: "docs://oscar64/manual",
549
+ name: "Oscar64 Manual",
550
+ description: "Oscar64 compiler manual",
551
+ mimeType: "text/markdown"
552
+ }
553
+ ];
554
+ };
555
+ const getResourceContent = async ({ uri }) => {
556
+ const state = getState();
557
+ if (uri === "index://quickstart") return { text: indexQuickstart() };
558
+ if (uri === "index://tooling-guide") return { text: indexToolingGuide() };
559
+ if (uri === "index://contracts") return { text: indexContracts() };
560
+ if (uri === "index://errors") return { text: indexErrors() };
561
+ if (uri === "index://status") {
562
+ return {
563
+ text: JSON.stringify(
564
+ {
565
+ source_keys: Object.keys(state.repos),
566
+ source_ready: Object.values(state.repos).every((r) => Boolean(r.rootDir)),
567
+ tutorial_count: state.tutorials.length,
568
+ manual_section_count: state.manualSections.length
569
+ },
570
+ null,
571
+ 2
572
+ )
573
+ };
574
+ }
575
+ if (uri === "index://manual-anchors") {
576
+ return {
577
+ text: JSON.stringify(
578
+ {
579
+ anchors: state.manualSections.map((s) => ({ anchor: s.anchor, heading: s.heading, level: s.level }))
580
+ },
581
+ null,
582
+ 2
583
+ )
584
+ };
585
+ }
586
+ if (uri === "index://tutorials") {
587
+ return {
588
+ text: JSON.stringify(
589
+ {
590
+ tutorials: state.tutorials.map((t) => tutorialManifest(t))
591
+ },
592
+ null,
593
+ 2
594
+ )
595
+ };
596
+ }
597
+ if (uri === "docs://oscar64/manual") {
598
+ return {
599
+ text: state.manualLines.join("\n")
600
+ };
601
+ }
602
+ if (uri.startsWith("docs://oscar64/manual#")) {
603
+ const anchor = uri.split("#")[1] ?? "";
604
+ const section = state.manualSections.find((s) => s.anchor === anchor);
605
+ if (!section) throw new Error(`Unknown anchor: ${anchor}`);
606
+ const content = state.manualLines.slice(section.startLine, section.endLine + 1).join("\n");
607
+ return {
608
+ text: content
609
+ };
610
+ }
611
+ if (uri.startsWith("tutorial://") && uri.endsWith("/manifest")) {
612
+ const id = uri.replace("tutorial://", "").replace("/manifest", "");
613
+ const t = state.tutorials.find((x) => x.id === id);
614
+ if (!t) throw new Error(`Unknown tutorial id: ${id}`);
615
+ return {
616
+ text: JSON.stringify(tutorialManifest(t), null, 2)
617
+ };
618
+ }
619
+ if (uri.startsWith("tutorial://") && uri.includes("/file/")) {
620
+ const rest = uri.replace("tutorial://", "");
621
+ const [id, , ...parts] = rest.split("/");
622
+ const rel = parts.join("/");
623
+ const t = state.tutorials.find((x) => x.id === id);
624
+ if (!t) throw new Error(`Unknown tutorial id: ${id}`);
625
+ const abs = safeJoin(t.absDir, rel);
626
+ const text = await safeReadText(abs);
627
+ return {
628
+ text
629
+ };
630
+ }
631
+ if (uri.startsWith("repo://oscar64/")) {
632
+ const rel = uri.replace("repo://oscar64/", "");
633
+ const abs = safeJoin(state.oscar64Root, rel);
634
+ if (!isTextPath(abs)) throw new Error("Only text files are readable");
635
+ const text = await safeReadText(abs);
636
+ return {
637
+ text
638
+ };
639
+ }
640
+ if (uri.startsWith("repo://OscarTutorials/")) {
641
+ const rel = uri.replace("repo://OscarTutorials/", "");
642
+ const abs = safeJoin(state.tutorialsRoot, rel);
643
+ if (!isTextPath(abs)) throw new Error("Only text files are readable");
644
+ const text = await safeReadText(abs);
645
+ return {
646
+ text
647
+ };
648
+ }
649
+ throw new Error(`Unsupported URI: ${uri}`);
650
+ };
651
+ return {
652
+ listResources,
653
+ getResourceContent,
654
+ resourceTemplates
655
+ };
656
+ }
657
+
658
+ // src/mcp/tools.ts
659
+ import { createTool } from "@mastra/core/tools";
660
+ import { z } from "zod";
661
+
662
+ // src/mcp/contract.ts
663
+ var CONTRACT_VERSION = "1.0";
664
+ function makeMeta(tool, state) {
665
+ void state;
666
+ return {
667
+ tool,
668
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
669
+ contract_version: CONTRACT_VERSION
670
+ };
671
+ }
672
+ function okResponse(tool, state, data, nextActions = []) {
673
+ return {
674
+ ok: true,
675
+ data,
676
+ next_actions: nextActions,
677
+ meta: makeMeta(tool, state)
678
+ };
679
+ }
680
+ function errorResponse(tool, state, error, nextActions = []) {
681
+ return {
682
+ ok: false,
683
+ error,
684
+ next_actions: nextActions,
685
+ meta: makeMeta(tool, state)
686
+ };
687
+ }
688
+
689
+ // src/mcp/tools.ts
690
+ function isAllowedScopePath(scope, relPath) {
691
+ const norm = relPath.replace(/\\/g, "/");
692
+ if (scope === "compiler") return norm.startsWith("oscar64/");
693
+ if (scope === "sdk") return norm.startsWith("include/");
694
+ if (scope === "samples") return norm.startsWith("samples/");
695
+ if (scope === "autotest") return norm.startsWith("autotest/");
696
+ if (scope === "tutorials") return true;
697
+ return true;
698
+ }
699
+ var standardEnvelopeOutput = z.object({
700
+ ok: z.boolean().describe("True on success, false on error."),
701
+ data: z.unknown().optional().describe("Tool-specific success payload."),
702
+ error: z.object({
703
+ code: z.string(),
704
+ message: z.string(),
705
+ hint: z.string(),
706
+ recoverable: z.boolean(),
707
+ suggested_tool_calls: z.array(z.object({ tool: z.string(), args: z.record(z.unknown()) })).optional()
708
+ }).optional(),
709
+ next_actions: z.array(
710
+ z.object({
711
+ action: z.string(),
712
+ reason: z.string(),
713
+ tool: z.string().optional(),
714
+ uri: z.string().optional()
715
+ })
716
+ ).optional(),
717
+ meta: z.object({
718
+ tool: z.string(),
719
+ generated_at: z.string(),
720
+ contract_version: z.string()
721
+ })
722
+ });
723
+ function makeTools(getState, refreshState) {
724
+ const searchDocs = createTool({
725
+ id: "search_docs",
726
+ description: "Use this first when you need conceptual guidance. Searches manual sections and tutorial manifests, then returns ranked URIs to read next.",
727
+ inputSchema: z.object({
728
+ query: z.string().min(1).describe("Natural language query, symbol, or feature name to search for."),
729
+ limit: z.number().int().min(1).max(50).default(10).describe("Maximum number of ranked matches to return."),
730
+ scope: z.enum(["manual", "tutorials", "sdk", "samples", "autotest", "all"]).default("all").describe("Search surface. Use 'manual' for docs and 'tutorials' for example routing.")
731
+ }),
732
+ outputSchema: standardEnvelopeOutput,
733
+ execute: async ({ context }) => {
734
+ const { query, limit, scope } = context;
735
+ const state = getState();
736
+ const filter = scope === "all" ? void 0 : (r) => {
737
+ if (scope === "manual") return r.kind === "manual_section";
738
+ if (scope === "tutorials") return r.kind === "tutorial";
739
+ return r.kind === "manual_section";
740
+ };
741
+ const results = state.docsSearch.search(query, {
742
+ combineWith: inferCombineMode(query),
743
+ prefix: true,
744
+ fuzzy: 0.12,
745
+ filter
746
+ }).slice(0, limit).map((r) => ({
747
+ uri: r.uri,
748
+ title: r.title,
749
+ snippet: r.excerpt,
750
+ kind: r.kind
751
+ }));
752
+ const nextActions = results.slice(0, 2).map((item) => ({
753
+ action: "Read top match",
754
+ reason: "Inspect exact source text before generating or modifying code.",
755
+ uri: item.uri
756
+ }));
757
+ return okResponse(
758
+ "search_docs",
759
+ state,
760
+ {
761
+ query,
762
+ scope,
763
+ limit,
764
+ results
765
+ },
766
+ nextActions
767
+ );
768
+ }
769
+ });
770
+ const getDocSection = createTool({
771
+ id: "get_doc_section",
772
+ description: "Use after search_docs when you need authoritative manual text. Resolves anchor/heading and returns a bounded section slice.",
773
+ inputSchema: z.object({
774
+ anchor_or_heading: z.string().min(1).describe("Manual anchor (preferred) or heading text to resolve and read."),
775
+ max_chars: z.number().int().min(1e3).max(25e3).default(12e3).describe("Maximum returned section length to control token usage.")
776
+ }),
777
+ outputSchema: standardEnvelopeOutput,
778
+ execute: async ({ context }) => {
779
+ const { anchor_or_heading, max_chars } = context;
780
+ const state = getState();
781
+ const q = anchor_or_heading.trim();
782
+ let matchedBy = null;
783
+ let section = state.manualSections.find((s) => s.anchor === q);
784
+ if (section) matchedBy = "anchor_exact";
785
+ if (!section) {
786
+ section = state.manualSections.find((s) => s.anchor === q.toLowerCase().replace(/\s+/g, "-"));
787
+ if (section) matchedBy = "anchor_normalized";
788
+ }
789
+ if (!section) {
790
+ section = state.manualSections.find((s) => s.heading.toLowerCase() === q.toLowerCase());
791
+ if (section) matchedBy = "heading_exact";
792
+ }
793
+ if (!section) {
794
+ section = state.manualSections.find((s) => s.heading.toLowerCase().includes(q.toLowerCase()));
795
+ if (section) matchedBy = "heading_fuzzy";
796
+ }
797
+ if (!section || !matchedBy) {
798
+ return errorResponse("get_doc_section", state, {
799
+ code: "NOT_FOUND",
800
+ message: `No manual section found for '${q}'.`,
801
+ hint: "Call search_docs first, then pass an exact returned manual URI anchor.",
802
+ recoverable: true,
803
+ suggested_tool_calls: [{ tool: "search_docs", args: { query: q, scope: "manual", limit: 5 } }]
804
+ });
805
+ }
806
+ const content = state.manualLines.slice(section.startLine, section.endLine + 1).join("\n").slice(0, max_chars);
807
+ const uri = `docs://oscar64/manual#${section.anchor}`;
808
+ return okResponse(
809
+ "get_doc_section",
810
+ state,
811
+ {
812
+ uri,
813
+ heading: section.heading,
814
+ anchor: section.anchor,
815
+ content,
816
+ truncated: content.length >= max_chars
817
+ },
818
+ [
819
+ {
820
+ action: "Search related implementation examples",
821
+ reason: "Pair manual guidance with concrete code patterns.",
822
+ tool: "search_code"
823
+ }
824
+ ]
825
+ );
826
+ }
827
+ });
828
+ const searchCode = createTool({
829
+ id: "search_code",
830
+ description: "Use when you need concrete source snippets. Returns file URIs plus line ranges and contextual snippets.",
831
+ inputSchema: z.object({
832
+ query: z.string().min(1).describe("Text or symbol to find in source files."),
833
+ scope: z.enum(["tutorials", "sdk", "samples", "autotest", "compiler", "all"]).default("tutorials").describe("Code corpus to search. Default is tutorials for practical examples."),
834
+ path_glob: z.string().optional().describe("Optional substring filter applied to relative file paths."),
835
+ limit: z.number().int().min(1).max(80).default(20).describe("Maximum number of snippet hits to return.")
836
+ }),
837
+ outputSchema: standardEnvelopeOutput,
838
+ execute: async ({ context }) => {
839
+ const { query, scope, path_glob, limit } = context;
840
+ const state = getState();
841
+ const q = query.toLowerCase();
842
+ const hits = [];
843
+ const roots = [];
844
+ if (scope === "tutorials" || scope === "all") {
845
+ roots.push({
846
+ repo: "OscarTutorials",
847
+ absRoot: state.tutorialsRoot
848
+ });
849
+ }
850
+ if (scope !== "tutorials") {
851
+ roots.push({
852
+ repo: "oscar64",
853
+ absRoot: state.oscar64Root
854
+ });
855
+ }
856
+ for (const root of roots) {
857
+ const files = await listFilesRecursive(root.absRoot);
858
+ for (const abs of files) {
859
+ if (!isTextPath(abs)) continue;
860
+ const rel = abs.replace(root.absRoot + "/", "").replace(/\\/g, "/");
861
+ if (path_glob && !rel.includes(path_glob)) continue;
862
+ if (root.repo === "oscar64" && scope !== "all" && scope !== "tutorials" && !isAllowedScopePath(scope, rel)) {
863
+ continue;
864
+ }
865
+ let text;
866
+ try {
867
+ text = await safeReadText(abs);
868
+ } catch {
869
+ continue;
870
+ }
871
+ const lines = text.split(/\r?\n/);
872
+ for (let i = 0; i < lines.length; i += 1) {
873
+ if (!lines[i].toLowerCase().includes(q)) continue;
874
+ const start = Math.max(0, i - 2);
875
+ const end = Math.min(lines.length - 1, i + 2);
876
+ const uri = root.repo === "oscar64" ? `repo://oscar64/${rel}` : `repo://OscarTutorials/${rel}`;
877
+ hits.push({
878
+ uri,
879
+ line_start: start + 1,
880
+ line_end: end + 1,
881
+ snippet: lines.slice(start, end + 1).join("\n").slice(0, 700)
882
+ });
883
+ if (hits.length >= limit) break;
884
+ }
885
+ if (hits.length >= limit) break;
886
+ }
887
+ if (hits.length >= limit) break;
888
+ }
889
+ return okResponse(
890
+ "search_code",
891
+ state,
892
+ {
893
+ query,
894
+ scope,
895
+ path_glob: path_glob ?? null,
896
+ limit,
897
+ results: hits
898
+ },
899
+ hits.length ? [
900
+ {
901
+ action: "Open top code hit",
902
+ reason: "Inspect concrete implementation before adaptation.",
903
+ uri: hits[0].uri
904
+ }
905
+ ] : []
906
+ );
907
+ }
908
+ });
909
+ const getBestTutorial = createTool({
910
+ id: "get_best_tutorial",
911
+ description: "Use to route a development task to the best tutorial starting points. Returns ranked recommendations with concept evidence.",
912
+ inputSchema: z.object({
913
+ task: z.string().min(1).describe("Plain-language coding task or feature goal."),
914
+ limit: z.number().int().min(1).max(10).default(3).describe("Maximum tutorial recommendations to return.")
915
+ }),
916
+ outputSchema: standardEnvelopeOutput,
917
+ execute: async ({ context }) => {
918
+ const { task, limit } = context;
919
+ const state = getState();
920
+ const expanded = expandTaskQuery(task);
921
+ const combineWith = inferCombineMode(expanded);
922
+ const scored = state.docsSearch.search(expanded, {
923
+ combineWith,
924
+ prefix: true,
925
+ fuzzy: 0.12,
926
+ filter: (r) => r.kind === "tutorial"
927
+ }).map((r) => {
928
+ const t = state.tutorials.find((x) => x.id === r.tutorialId);
929
+ return { r, t };
930
+ }).filter((x) => Boolean(x.t)).sort((a, b) => b.r.score - a.r.score || a.t.id.localeCompare(b.t.id, void 0, { numeric: true })).slice(0, limit).map((x) => ({
931
+ tutorial_uri: `tutorial://${x.t.id}/manifest`,
932
+ tutorial_id: x.t.id,
933
+ title: `${x.t.id} ${x.t.title}`,
934
+ category: x.t.category,
935
+ concepts: x.t.concepts,
936
+ key_files: x.t.files.slice(0, 12),
937
+ asset_refs: x.t.assetRefs,
938
+ preview: x.t.previewText.slice(0, 320)
939
+ }));
940
+ if (scored.length === 0) {
941
+ return errorResponse("get_best_tutorial", state, {
942
+ code: "NOT_FOUND",
943
+ message: `No tutorial candidates matched '${task}'.`,
944
+ hint: "Try broader terms or call search_code with scope='tutorials' to inspect raw source hits.",
945
+ recoverable: true,
946
+ suggested_tool_calls: [
947
+ { tool: "search_docs", args: { query: task, scope: "tutorials", limit: 10 } },
948
+ { tool: "search_code", args: { query: task, scope: "tutorials", limit: 20 } }
949
+ ]
950
+ });
951
+ }
952
+ return okResponse(
953
+ "get_best_tutorial",
954
+ state,
955
+ {
956
+ task,
957
+ recommendations: scored
958
+ },
959
+ [
960
+ {
961
+ action: "Read top tutorial manifest",
962
+ reason: "Validate concept fit and inspect key files before coding.",
963
+ uri: scored[0].tutorial_uri
964
+ }
965
+ ]
966
+ );
967
+ }
968
+ });
969
+ const refreshSources = createTool({
970
+ id: "refresh_sources",
971
+ description: "Refresh source snapshots.",
972
+ inputSchema: z.object({
973
+ force: z.boolean().default(false).describe("When true, bypass staleness window and force remote refresh checks.")
974
+ }),
975
+ outputSchema: standardEnvelopeOutput,
976
+ execute: async ({ context }) => {
977
+ const { force } = context;
978
+ await refreshState(force);
979
+ const state = getState();
980
+ return okResponse("refresh_sources", state, {
981
+ refreshed: true,
982
+ force,
983
+ source_keys: Object.keys(state.repos),
984
+ source_ready: Object.values(state.repos).every((r) => Boolean(r.rootDir))
985
+ });
986
+ }
987
+ });
988
+ const getSourceStatus = createTool({
989
+ id: "get_source_status",
990
+ description: "Get source readiness and index sizes.",
991
+ inputSchema: z.object({}),
992
+ outputSchema: standardEnvelopeOutput,
993
+ execute: async () => {
994
+ const state = getState();
995
+ return okResponse("get_source_status", state, {
996
+ source_keys: Object.keys(state.repos),
997
+ source_ready: Object.values(state.repos).every((r) => Boolean(r.rootDir)),
998
+ tutorial_count: state.tutorials.length,
999
+ manual_section_count: state.manualSections.length
1000
+ });
1001
+ }
1002
+ });
1003
+ return {
1004
+ searchDocs,
1005
+ getDocSection,
1006
+ searchCode,
1007
+ getBestTutorial,
1008
+ refreshSources,
1009
+ getSourceStatus
1010
+ };
1011
+ }
1012
+
1013
+ // src/state.ts
1014
+ import fs4 from "fs/promises";
1015
+ import path5 from "path";
1016
+
1017
+ // src/indexing/manual.ts
1018
+ function parseManualSections(text) {
1019
+ const lines = text.split(/\r?\n/);
1020
+ const headings = [];
1021
+ const seenAnchors = /* @__PURE__ */ new Map();
1022
+ for (let i = 0; i < lines.length; i += 1) {
1023
+ const match = lines[i].match(/^(#{1,6})\s+(.+?)\s*$/);
1024
+ if (!match) continue;
1025
+ const baseAnchor = slugify(match[2]);
1026
+ const seen = seenAnchors.get(baseAnchor) ?? 0;
1027
+ seenAnchors.set(baseAnchor, seen + 1);
1028
+ const anchor = seen === 0 ? baseAnchor : `${baseAnchor}-${seen}`;
1029
+ headings.push({
1030
+ line: i,
1031
+ level: match[1].length,
1032
+ heading: match[2].trim(),
1033
+ anchor
1034
+ });
1035
+ }
1036
+ const sections = headings.map((h, idx) => ({
1037
+ anchor: h.anchor,
1038
+ heading: h.heading,
1039
+ level: h.level,
1040
+ startLine: h.line,
1041
+ endLine: idx + 1 < headings.length ? headings[idx + 1].line - 1 : lines.length - 1
1042
+ }));
1043
+ return { lines, sections };
1044
+ }
1045
+
1046
+ // src/sync/github-http.ts
1047
+ import AdmZip from "adm-zip";
1048
+ import fs3 from "fs/promises";
1049
+ import path4 from "path";
1050
+ function githubHeaders() {
1051
+ const headers = {
1052
+ "User-Agent": "oscar64-mcp-docs",
1053
+ Accept: "application/vnd.github+json"
1054
+ };
1055
+ const token = process.env.GITHUB_TOKEN;
1056
+ if (token) headers.Authorization = `Bearer ${token}`;
1057
+ return headers;
1058
+ }
1059
+ async function fetchJson(url) {
1060
+ const res = await fetch(url, { headers: githubHeaders() });
1061
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
1062
+ return await res.json();
1063
+ }
1064
+ async function getRemoteSha(cfg) {
1065
+ const data = await fetchJson(`${GITHUB_API}/repos/${cfg.owner}/${cfg.repo}/commits/${cfg.trackingRef}`);
1066
+ return data.sha;
1067
+ }
1068
+ async function downloadZip(cfg, ref) {
1069
+ const url = `${GITHUB_API}/repos/${cfg.owner}/${cfg.repo}/zipball/${ref}`;
1070
+ const res = await fetch(url, { headers: githubHeaders(), redirect: "follow" });
1071
+ if (!res.ok) throw new Error(`Zip download failed: HTTP ${res.status}`);
1072
+ const ab = await res.arrayBuffer();
1073
+ return Buffer.from(ab);
1074
+ }
1075
+ async function unpackZipToDir(zipBuf, targetDir) {
1076
+ const tmpDir = `${targetDir}.tmp`;
1077
+ await fs3.rm(tmpDir, { recursive: true, force: true });
1078
+ await fs3.mkdir(tmpDir, { recursive: true });
1079
+ const zip = new AdmZip(zipBuf);
1080
+ const entries = zip.getEntries();
1081
+ for (const entry of entries) {
1082
+ const normalized = entry.entryName.replace(/\\/g, "/");
1083
+ const parts = normalized.split("/").filter(Boolean);
1084
+ if (parts.length <= 1) continue;
1085
+ const relPath = parts.slice(1).join("/");
1086
+ if (!relPath) continue;
1087
+ const abs = safeJoin(tmpDir, relPath);
1088
+ if (entry.isDirectory) {
1089
+ await fs3.mkdir(abs, { recursive: true });
1090
+ continue;
1091
+ }
1092
+ await fs3.mkdir(path4.dirname(abs), { recursive: true });
1093
+ await fs3.writeFile(abs, entry.getData());
1094
+ }
1095
+ await fs3.rm(targetDir, { recursive: true, force: true });
1096
+ await fs3.rename(tmpDir, targetDir);
1097
+ }
1098
+ async function syncRepo(cfg, force = false) {
1099
+ await fs3.mkdir(META_DIR, { recursive: true });
1100
+ await fs3.mkdir(SOURCES_DIR, { recursive: true });
1101
+ const metaPath = path4.join(META_DIR, `${cfg.key}.json`);
1102
+ const sourceRoot = path4.join(SOURCES_DIR, cfg.key, "current");
1103
+ const existing = await readJson(metaPath);
1104
+ const now = Date.now();
1105
+ const stale = !existing?.lastCheckedAt || now - existing.lastCheckedAt > UPDATE_TTL_MS;
1106
+ if (!force && !stale && await exists(sourceRoot) && existing?.resolvedSha) {
1107
+ return {
1108
+ key: cfg.key,
1109
+ rootDir: sourceRoot,
1110
+ resolvedSha: existing.resolvedSha,
1111
+ trackingRef: cfg.trackingRef,
1112
+ lastZipHash: existing.lastZipHash,
1113
+ lastCheckedAt: existing.lastCheckedAt,
1114
+ stale: false
1115
+ };
1116
+ }
1117
+ let remoteSha;
1118
+ try {
1119
+ remoteSha = await getRemoteSha(cfg);
1120
+ } catch (error) {
1121
+ if (await exists(sourceRoot) && existing?.resolvedSha) {
1122
+ return {
1123
+ key: cfg.key,
1124
+ rootDir: sourceRoot,
1125
+ resolvedSha: existing.resolvedSha,
1126
+ trackingRef: cfg.trackingRef,
1127
+ lastZipHash: existing.lastZipHash,
1128
+ lastCheckedAt: existing.lastCheckedAt,
1129
+ stale: true
1130
+ };
1131
+ }
1132
+ throw error;
1133
+ }
1134
+ if (!force && remoteSha === existing?.lastRemoteSha && await exists(sourceRoot) && existing?.resolvedSha) {
1135
+ const refreshed = {
1136
+ ...existing,
1137
+ trackingRef: cfg.trackingRef,
1138
+ sourceUrl: `https://github.com/${cfg.owner}/${cfg.repo}`,
1139
+ lastCheckedAt: now
1140
+ };
1141
+ await writeJson(metaPath, refreshed);
1142
+ return {
1143
+ key: cfg.key,
1144
+ rootDir: sourceRoot,
1145
+ resolvedSha: existing.resolvedSha,
1146
+ trackingRef: cfg.trackingRef,
1147
+ lastZipHash: existing.lastZipHash,
1148
+ lastCheckedAt: now,
1149
+ stale: false
1150
+ };
1151
+ }
1152
+ const zipBuf = await downloadZip(cfg, remoteSha);
1153
+ const zipHash = sha256(zipBuf);
1154
+ if (!force && zipHash === existing?.lastZipHash && await exists(sourceRoot) && existing?.resolvedSha) {
1155
+ const refreshed = {
1156
+ ...existing,
1157
+ trackingRef: cfg.trackingRef,
1158
+ sourceUrl: `https://github.com/${cfg.owner}/${cfg.repo}`,
1159
+ lastRemoteSha: remoteSha,
1160
+ lastCheckedAt: now,
1161
+ updatedAt: now
1162
+ };
1163
+ await writeJson(metaPath, refreshed);
1164
+ return {
1165
+ key: cfg.key,
1166
+ rootDir: sourceRoot,
1167
+ resolvedSha: existing.resolvedSha,
1168
+ trackingRef: cfg.trackingRef,
1169
+ lastZipHash: zipHash,
1170
+ lastCheckedAt: now,
1171
+ stale: false
1172
+ };
1173
+ }
1174
+ await unpackZipToDir(zipBuf, sourceRoot);
1175
+ const nextMeta = {
1176
+ sourceUrl: `https://github.com/${cfg.owner}/${cfg.repo}`,
1177
+ trackingRef: cfg.trackingRef,
1178
+ resolvedSha: remoteSha,
1179
+ lastRemoteSha: remoteSha,
1180
+ lastZipHash: zipHash,
1181
+ lastCheckedAt: now,
1182
+ updatedAt: now
1183
+ };
1184
+ await writeJson(metaPath, nextMeta);
1185
+ return {
1186
+ key: cfg.key,
1187
+ rootDir: sourceRoot,
1188
+ resolvedSha: remoteSha,
1189
+ trackingRef: cfg.trackingRef,
1190
+ lastZipHash: zipHash,
1191
+ lastCheckedAt: now,
1192
+ stale: false
1193
+ };
1194
+ }
1195
+ async function syncAllRepos(force = false) {
1196
+ const states = await Promise.all(REPOS.map((repo) => syncRepo(repo, force)));
1197
+ return Object.fromEntries(states.map((s) => [s.key, s]));
1198
+ }
1199
+
1200
+ // src/state.ts
1201
+ async function initState(forceRefresh = false) {
1202
+ await fs4.mkdir(CACHE_DIR, { recursive: true });
1203
+ await fs4.mkdir(INDEX_DIR, { recursive: true });
1204
+ const repos = await syncAllRepos(forceRefresh);
1205
+ const oscar64Root = repos.oscar64.rootDir;
1206
+ const tutorialsRoot = repos.OscarTutorials.rootDir;
1207
+ const manualPath = path5.join(oscar64Root, "oscar64.md");
1208
+ const manualText = await safeReadText(manualPath);
1209
+ const { lines: manualLines, sections: manualSections } = parseManualSections(manualText);
1210
+ const tutorials = await buildTutorialsIndex(tutorialsRoot);
1211
+ const baseState = {
1212
+ repos,
1213
+ oscar64Root,
1214
+ tutorialsRoot,
1215
+ manualLines,
1216
+ manualSections,
1217
+ tutorials
1218
+ };
1219
+ return {
1220
+ ...baseState,
1221
+ docsSearch: buildDocsSearch(baseState)
1222
+ };
1223
+ }
1224
+
1225
+ // src/mcp/server.ts
1226
+ async function createOscarMcpServer(initialForceRefresh = false) {
1227
+ let state = await initState(initialForceRefresh);
1228
+ const getState = () => state;
1229
+ const refreshState = async (force = false) => {
1230
+ state = await initState(force);
1231
+ };
1232
+ const resources = makeResources(getState);
1233
+ const tools = makeTools(getState, refreshState);
1234
+ const server = new MCPServer({
1235
+ id: "oscar64-docs-examples",
1236
+ name: "Oscar64 Docs And Examples",
1237
+ version: "0.1.0",
1238
+ description: "Local MCP server exposing Oscar64 docs, SDK, samples, and OscarTutorials with HTTP zip sync and hash-based freshness checks.",
1239
+ tools: {
1240
+ search_docs: tools.searchDocs,
1241
+ get_doc_section: tools.getDocSection,
1242
+ search_code: tools.searchCode,
1243
+ get_best_tutorial: tools.getBestTutorial,
1244
+ refresh_sources: tools.refreshSources,
1245
+ get_source_status: tools.getSourceStatus
1246
+ },
1247
+ resources
1248
+ });
1249
+ return { server, getState, refreshState };
1250
+ }
1251
+
1252
+ // src/stdio.ts
1253
+ async function main() {
1254
+ const { server } = await createOscarMcpServer(false);
1255
+ await server.startStdio();
1256
+ }
1257
+ main().catch((error) => {
1258
+ console.error("Failed to start MCP server:", error);
1259
+ process.exit(1);
1260
+ });