llm-wiki-mcp 0.1.0

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/server.js ADDED
@@ -0,0 +1,895 @@
1
+ // src/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/server";
3
+
4
+ // src/config/schema-loader.ts
5
+ import { readFileSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+ import yaml from "js-yaml";
8
+
9
+ // src/config/types.ts
10
+ var DEFAULT_SCHEMA = {
11
+ name: "My Wiki",
12
+ version: 1,
13
+ linkStyle: "wikilink",
14
+ paths: {
15
+ raw: "raw",
16
+ wiki: "wiki",
17
+ assets: "raw/assets"
18
+ },
19
+ pageTypes: {
20
+ source: {
21
+ description: "Summary of a raw source document",
22
+ requiredFields: ["title", "type", "source_path", "created"]
23
+ },
24
+ concept: {
25
+ description: "A concept or idea",
26
+ requiredFields: ["title", "type", "tags", "created"]
27
+ },
28
+ entity: {
29
+ description: "A person, organization, or thing",
30
+ requiredFields: ["title", "type", "tags", "created"]
31
+ },
32
+ comparison: {
33
+ description: "Comparison between concepts/entities",
34
+ requiredFields: ["title", "type", "subjects", "created"]
35
+ }
36
+ },
37
+ tags: {
38
+ required: false,
39
+ suggested: []
40
+ },
41
+ log: {
42
+ prefix: "## [{date}] {operation} | {title}"
43
+ }
44
+ };
45
+
46
+ // src/config/schema-loader.ts
47
+ function loadSchema(vaultPath) {
48
+ const configPath = join(vaultPath, ".wiki-schema.yaml");
49
+ if (!existsSync(configPath)) {
50
+ return { ...DEFAULT_SCHEMA };
51
+ }
52
+ const raw = readFileSync(configPath, "utf-8");
53
+ const parsed = yaml.load(raw);
54
+ if (parsed.linkStyle && parsed.linkStyle !== "wikilink" && parsed.linkStyle !== "markdown") {
55
+ throw new Error(
56
+ `Invalid linkStyle: "${parsed.linkStyle}". Must be "wikilink" or "markdown".`
57
+ );
58
+ }
59
+ return {
60
+ name: parsed.name ?? DEFAULT_SCHEMA.name,
61
+ version: parsed.version ?? DEFAULT_SCHEMA.version,
62
+ linkStyle: parsed.linkStyle ?? DEFAULT_SCHEMA.linkStyle,
63
+ paths: {
64
+ ...DEFAULT_SCHEMA.paths,
65
+ ...parsed.paths
66
+ },
67
+ pageTypes: parsed.pageTypes ?? DEFAULT_SCHEMA.pageTypes,
68
+ tags: {
69
+ ...DEFAULT_SCHEMA.tags,
70
+ ...parsed.tags
71
+ },
72
+ log: {
73
+ ...DEFAULT_SCHEMA.log,
74
+ ...parsed.log
75
+ }
76
+ };
77
+ }
78
+
79
+ // src/core/link-resolver.ts
80
+ var WIKILINK_REGEX = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
81
+ var MDLINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
82
+ var LinkResolver = class {
83
+ constructor(linkStyle, wikiDir) {
84
+ this.linkStyle = linkStyle;
85
+ this.wikiDir = wikiDir;
86
+ }
87
+ linkStyle;
88
+ wikiDir;
89
+ slugify(title) {
90
+ return title.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
91
+ }
92
+ createLink(targetTitle, displayText) {
93
+ if (this.linkStyle === "wikilink") {
94
+ if (displayText) {
95
+ return `[[${targetTitle}|${displayText}]]`;
96
+ }
97
+ return `[[${targetTitle}]]`;
98
+ }
99
+ const slug = this.slugify(targetTitle);
100
+ const path = `${this.wikiDir}/${slug}.md`;
101
+ const text = displayText ?? targetTitle;
102
+ return `[${text}](${path})`;
103
+ }
104
+ parseLinks(content) {
105
+ const links = [];
106
+ if (this.linkStyle === "wikilink") {
107
+ let match;
108
+ const regex = new RegExp(WIKILINK_REGEX.source, "g");
109
+ while ((match = regex.exec(content)) !== null) {
110
+ links.push({
111
+ raw: match[0],
112
+ target: match[1],
113
+ displayText: match[2] ?? void 0
114
+ });
115
+ }
116
+ } else {
117
+ let match;
118
+ const regex = new RegExp(MDLINK_REGEX.source, "g");
119
+ while ((match = regex.exec(content)) !== null) {
120
+ links.push({
121
+ raw: match[0],
122
+ target: match[1],
123
+ displayText: void 0
124
+ });
125
+ }
126
+ }
127
+ return links;
128
+ }
129
+ };
130
+
131
+ // src/core/log-manager.ts
132
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, appendFileSync } from "fs";
133
+ import { join as join2 } from "path";
134
+ var LOG_HEADER = "# Wiki Log\n";
135
+ var ENTRY_REGEX = /^## \[(\d{4}-\d{2}-\d{2})\] (\w+) \| (.+)$/;
136
+ var LogManager = class {
137
+ logPath;
138
+ constructor(vaultPath) {
139
+ this.logPath = join2(vaultPath, "log.md");
140
+ }
141
+ async append(entry) {
142
+ if (!existsSync2(this.logPath)) {
143
+ writeFileSync(this.logPath, LOG_HEADER + "\n", "utf-8");
144
+ }
145
+ let block = `
146
+ ## [${entry.date}] ${entry.operation} | ${entry.title}
147
+ `;
148
+ if (entry.details) {
149
+ block += `${entry.details}
150
+ `;
151
+ }
152
+ appendFileSync(this.logPath, block, "utf-8");
153
+ }
154
+ async read(filter) {
155
+ if (!existsSync2(this.logPath)) {
156
+ return [];
157
+ }
158
+ const content = readFileSync2(this.logPath, "utf-8");
159
+ const lines = content.split("\n");
160
+ const entries = [];
161
+ let current = null;
162
+ const detailLines = [];
163
+ const flushCurrent = () => {
164
+ if (current) {
165
+ if (detailLines.length > 0) {
166
+ current.details = detailLines.join("\n").trim() || void 0;
167
+ }
168
+ entries.push(current);
169
+ detailLines.length = 0;
170
+ }
171
+ };
172
+ for (const line of lines) {
173
+ const match = line.match(ENTRY_REGEX);
174
+ if (match) {
175
+ flushCurrent();
176
+ current = {
177
+ date: match[1],
178
+ operation: match[2],
179
+ title: match[3]
180
+ };
181
+ } else if (current && line.trim() !== "" && !line.startsWith("# ")) {
182
+ detailLines.push(line);
183
+ }
184
+ }
185
+ flushCurrent();
186
+ let result = entries;
187
+ if (filter?.operation) {
188
+ result = result.filter((e) => e.operation === filter.operation);
189
+ }
190
+ if (filter?.since) {
191
+ result = result.filter((e) => e.date >= filter.since);
192
+ }
193
+ if (filter?.limit) {
194
+ result = result.slice(-filter.limit);
195
+ }
196
+ return result;
197
+ }
198
+ };
199
+
200
+ // src/core/index-manager.ts
201
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
202
+ import { join as join3 } from "path";
203
+ var INDEX_HEADER = "# Wiki Index\n";
204
+ var CATEGORY_MAP = {
205
+ source: "Sources",
206
+ concept: "Concepts",
207
+ entity: "Entities",
208
+ comparison: "Comparisons"
209
+ };
210
+ var IndexManager = class {
211
+ constructor(vaultPath, linkResolver) {
212
+ this.linkResolver = linkResolver;
213
+ this.indexPath = join3(vaultPath, "index.md");
214
+ }
215
+ linkResolver;
216
+ indexPath;
217
+ async addEntry(entry) {
218
+ const stored = this.load();
219
+ stored.entries.push(entry);
220
+ this.save(stored);
221
+ }
222
+ async removeEntry(path) {
223
+ const stored = this.load();
224
+ stored.entries = stored.entries.filter((e) => e.path !== path);
225
+ this.save(stored);
226
+ }
227
+ async read() {
228
+ return this.load().entries;
229
+ }
230
+ async rebuild(entries) {
231
+ this.save({ entries });
232
+ }
233
+ load() {
234
+ if (!existsSync3(this.indexPath)) {
235
+ return { entries: [] };
236
+ }
237
+ const content = readFileSync3(this.indexPath, "utf-8");
238
+ const entries = [];
239
+ const pathCommentRegex = /<!--\s*path:\s*([^>]+?)\s*-->/;
240
+ const summaryRegex = /^- .+? — (.+?)(?:\s*<!--.*-->)?$/;
241
+ const wikilinkRegex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/;
242
+ const mdlinkRegex = /\[([^\]]+)\]\(([^)]+)\)/;
243
+ let currentType = "";
244
+ const categoryToType = {};
245
+ for (const [type, category] of Object.entries(CATEGORY_MAP)) {
246
+ categoryToType[category] = type;
247
+ }
248
+ for (const line of content.split("\n")) {
249
+ if (line.startsWith("## ")) {
250
+ const category = line.slice(3).trim();
251
+ currentType = categoryToType[category] ?? "other";
252
+ continue;
253
+ }
254
+ if (!line.startsWith("- ")) continue;
255
+ const summaryMatch = line.match(summaryRegex);
256
+ const summary = summaryMatch?.[1]?.trim() ?? "";
257
+ const pathMatch = line.match(pathCommentRegex);
258
+ const storedPath = pathMatch?.[1] ?? "";
259
+ const wikiMatch = line.match(wikilinkRegex);
260
+ const mdMatch = line.match(mdlinkRegex);
261
+ if (wikiMatch) {
262
+ entries.push({
263
+ title: wikiMatch[1],
264
+ path: storedPath,
265
+ pageType: currentType,
266
+ summary
267
+ });
268
+ } else if (mdMatch) {
269
+ entries.push({
270
+ title: mdMatch[1],
271
+ path: storedPath || mdMatch[2],
272
+ pageType: currentType,
273
+ summary
274
+ });
275
+ }
276
+ }
277
+ return { entries };
278
+ }
279
+ save(stored) {
280
+ const grouped = {};
281
+ for (const entry of stored.entries) {
282
+ const category = CATEGORY_MAP[entry.pageType] ?? "Other";
283
+ if (!grouped[category]) {
284
+ grouped[category] = [];
285
+ }
286
+ grouped[category].push(entry);
287
+ }
288
+ let content = INDEX_HEADER;
289
+ const orderedCategories = [
290
+ "Sources",
291
+ "Concepts",
292
+ "Entities",
293
+ "Comparisons",
294
+ "Other"
295
+ ];
296
+ for (const category of orderedCategories) {
297
+ const entries = grouped[category];
298
+ if (!entries || entries.length === 0) continue;
299
+ content += `
300
+ ## ${category}
301
+
302
+ `;
303
+ for (const entry of entries) {
304
+ const link = this.linkResolver.createLink(entry.title);
305
+ content += `- ${link} \u2014 ${entry.summary} <!-- path: ${entry.path} -->
306
+ `;
307
+ }
308
+ }
309
+ writeFileSync2(this.indexPath, content, "utf-8");
310
+ }
311
+ };
312
+
313
+ // src/core/wiki-manager.ts
314
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync, readdirSync } from "fs";
315
+ import { join as join4 } from "path";
316
+ import matter from "gray-matter";
317
+ var WikiManager = class {
318
+ constructor(vaultPath, schema, linkResolver, indexManager, logManager) {
319
+ this.vaultPath = vaultPath;
320
+ this.schema = schema;
321
+ this.linkResolver = linkResolver;
322
+ this.indexManager = indexManager;
323
+ this.logManager = logManager;
324
+ this.wikiAbsDir = join4(vaultPath, schema.paths.wiki);
325
+ }
326
+ vaultPath;
327
+ schema;
328
+ linkResolver;
329
+ indexManager;
330
+ logManager;
331
+ wikiAbsDir;
332
+ async createPage(title, content, pageType) {
333
+ const slug = this.linkResolver.slugify(title);
334
+ const relativePath = `${this.schema.paths.wiki}/${slug}.md`;
335
+ const absPath = join4(this.vaultPath, relativePath);
336
+ if (existsSync4(absPath)) {
337
+ return { success: false, path: relativePath, message: `Page already exists: ${relativePath}` };
338
+ }
339
+ writeFileSync3(absPath, content, "utf-8");
340
+ const parsed = matter(content);
341
+ const summary = this.extractSummary(parsed.content);
342
+ await this.indexManager.addEntry({
343
+ title,
344
+ path: relativePath,
345
+ pageType: pageType ?? parsed.data.type ?? "other",
346
+ summary
347
+ });
348
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
349
+ await this.logManager.append({
350
+ date: today,
351
+ operation: "create",
352
+ title,
353
+ details: `Created: ${relativePath}`
354
+ });
355
+ return { success: true, path: relativePath, message: `Created: ${relativePath}` };
356
+ }
357
+ async readPage(lookup) {
358
+ let absPath;
359
+ let relativePath;
360
+ if (lookup.path) {
361
+ relativePath = lookup.path;
362
+ absPath = join4(this.vaultPath, relativePath);
363
+ } else if (lookup.title) {
364
+ const slug = this.linkResolver.slugify(lookup.title);
365
+ relativePath = `${this.schema.paths.wiki}/${slug}.md`;
366
+ absPath = join4(this.vaultPath, relativePath);
367
+ } else {
368
+ throw new Error("Either title or path must be provided");
369
+ }
370
+ if (!existsSync4(absPath)) {
371
+ throw new Error(`Page not found: ${relativePath}`);
372
+ }
373
+ const raw = readFileSync4(absPath, "utf-8");
374
+ const parsed = matter(raw);
375
+ return {
376
+ content: raw,
377
+ frontmatter: parsed.data,
378
+ path: relativePath
379
+ };
380
+ }
381
+ async updatePage(path, content) {
382
+ const absPath = join4(this.vaultPath, path);
383
+ if (!existsSync4(absPath)) {
384
+ return { success: false, path, message: `Page not found: ${path}` };
385
+ }
386
+ writeFileSync3(absPath, content, "utf-8");
387
+ const parsed = matter(content);
388
+ const title = parsed.data.title ?? path;
389
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
390
+ await this.logManager.append({
391
+ date: today,
392
+ operation: "update",
393
+ title,
394
+ details: `Updated: ${path}`
395
+ });
396
+ return { success: true, path, message: `Updated: ${path}` };
397
+ }
398
+ async deletePage(path) {
399
+ const absPath = join4(this.vaultPath, path);
400
+ if (!existsSync4(absPath)) {
401
+ return { success: false, message: `Page not found: ${path}`, brokenLinks: [] };
402
+ }
403
+ const raw = readFileSync4(absPath, "utf-8");
404
+ const parsed = matter(raw);
405
+ const title = parsed.data.title ?? path;
406
+ const brokenLinks = this.findBacklinks(title);
407
+ unlinkSync(absPath);
408
+ await this.indexManager.removeEntry(path);
409
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
410
+ await this.logManager.append({
411
+ date: today,
412
+ operation: "delete",
413
+ title,
414
+ details: `Deleted: ${path}. Broken links in: ${brokenLinks.join(", ") || "none"}`
415
+ });
416
+ return { success: true, message: `Deleted: ${path}`, brokenLinks };
417
+ }
418
+ findBacklinks(title) {
419
+ const backlinks = [];
420
+ const files = this.listWikiFiles();
421
+ for (const file of files) {
422
+ const absPath = join4(this.vaultPath, file);
423
+ const content = readFileSync4(absPath, "utf-8");
424
+ const links = this.linkResolver.parseLinks(content);
425
+ if (links.some((l) => l.target === title)) {
426
+ backlinks.push(file);
427
+ }
428
+ }
429
+ return backlinks;
430
+ }
431
+ listWikiFiles() {
432
+ if (!existsSync4(this.wikiAbsDir)) return [];
433
+ return readdirSync(this.wikiAbsDir).filter((f) => f.endsWith(".md")).map((f) => `${this.schema.paths.wiki}/${f}`);
434
+ }
435
+ extractSummary(content) {
436
+ const lines = content.trim().split("\n");
437
+ for (const line of lines) {
438
+ const trimmed = line.trim();
439
+ if (trimmed && !trimmed.startsWith("#")) {
440
+ return trimmed.slice(0, 120);
441
+ }
442
+ }
443
+ return "";
444
+ }
445
+ };
446
+
447
+ // src/search/qmd-provider.ts
448
+ import { execFile } from "child_process";
449
+ import { promisify } from "util";
450
+ var execFileAsync = promisify(execFile);
451
+ var QmdProvider = class {
452
+ name = "qmd";
453
+ async available() {
454
+ try {
455
+ await execFileAsync("which", ["qmd"]);
456
+ return true;
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+ async index(wikiDir) {
462
+ try {
463
+ await execFileAsync("qmd", ["index", "--dir", wikiDir]);
464
+ } catch (err) {
465
+ throw new Error(`qmd index failed: ${err}`);
466
+ }
467
+ }
468
+ async search(query, options) {
469
+ const { maxResults, wikiDir } = options;
470
+ try {
471
+ const { stdout } = await execFileAsync("qmd", [
472
+ "search",
473
+ query,
474
+ "--dir",
475
+ wikiDir,
476
+ "--limit",
477
+ String(maxResults),
478
+ "--json"
479
+ ]);
480
+ const parsed = JSON.parse(stdout);
481
+ if (!Array.isArray(parsed)) return [];
482
+ return parsed.map((item) => ({
483
+ path: String(item.path ?? ""),
484
+ title: String(item.title ?? ""),
485
+ snippet: String(item.snippet ?? item.content ?? ""),
486
+ score: Number(item.score ?? 0)
487
+ }));
488
+ } catch {
489
+ return [];
490
+ }
491
+ }
492
+ };
493
+
494
+ // src/search/simple-provider.ts
495
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
496
+ import { join as join5 } from "path";
497
+ import matter2 from "gray-matter";
498
+ var SimpleProvider = class {
499
+ name = "simple";
500
+ async available() {
501
+ return true;
502
+ }
503
+ async index() {
504
+ }
505
+ async search(query, options) {
506
+ const { maxResults, wikiDir } = options;
507
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
508
+ if (terms.length === 0) return [];
509
+ const files = readdirSync2(wikiDir).filter((f) => f.endsWith(".md"));
510
+ const scored = [];
511
+ for (const file of files) {
512
+ const absPath = join5(wikiDir, file);
513
+ const raw = readFileSync5(absPath, "utf-8");
514
+ const parsed = matter2(raw);
515
+ const title = parsed.data.title ?? file.replace(".md", "");
516
+ const body = parsed.content.toLowerCase();
517
+ const titleLower = title.toLowerCase();
518
+ let score = 0;
519
+ let matchedSnippet = "";
520
+ for (const term of terms) {
521
+ if (titleLower.includes(term)) {
522
+ score += 3;
523
+ }
524
+ const bodyMatches = body.split(term).length - 1;
525
+ score += bodyMatches;
526
+ if (!matchedSnippet && bodyMatches > 0) {
527
+ const idx = body.indexOf(term);
528
+ const start = Math.max(0, idx - 50);
529
+ const end = Math.min(body.length, idx + term.length + 100);
530
+ matchedSnippet = parsed.content.slice(start, end).trim();
531
+ }
532
+ }
533
+ if (score > 0) {
534
+ scored.push({
535
+ path: file,
536
+ title,
537
+ snippet: matchedSnippet,
538
+ score: Math.min(score / (terms.length * 5), 1)
539
+ });
540
+ }
541
+ }
542
+ scored.sort((a, b) => b.score - a.score);
543
+ return scored.slice(0, maxResults);
544
+ }
545
+ };
546
+
547
+ // src/search/detect.ts
548
+ async function detectSearchProvider() {
549
+ const qmd = new QmdProvider();
550
+ if (await qmd.available()) {
551
+ return qmd;
552
+ }
553
+ return new SimpleProvider();
554
+ }
555
+
556
+ // src/tools/index.ts
557
+ import { z } from "zod";
558
+
559
+ // src/tools/init.ts
560
+ import { mkdirSync, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
561
+ import { join as join6 } from "path";
562
+ import yaml2 from "js-yaml";
563
+ async function handleInit(input) {
564
+ const { path: vaultPath, name, linkStyle } = input;
565
+ const schemaPath = join6(vaultPath, ".wiki-schema.yaml");
566
+ if (existsSync5(schemaPath)) {
567
+ return {
568
+ success: true,
569
+ message: "Vault already initialized \u2014 skipping without overwrite.",
570
+ created: []
571
+ };
572
+ }
573
+ const schema = {
574
+ ...DEFAULT_SCHEMA,
575
+ name: name ?? DEFAULT_SCHEMA.name,
576
+ linkStyle: linkStyle ?? DEFAULT_SCHEMA.linkStyle
577
+ };
578
+ const created = [];
579
+ const dirs = [schema.paths.raw, schema.paths.assets, schema.paths.wiki];
580
+ for (const dir of dirs) {
581
+ const absDir = join6(vaultPath, dir);
582
+ if (!existsSync5(absDir)) {
583
+ mkdirSync(absDir, { recursive: true });
584
+ created.push(dir + "/");
585
+ }
586
+ }
587
+ const yamlContent = yaml2.dump(schema, { lineWidth: -1 });
588
+ writeFileSync4(schemaPath, yamlContent, "utf-8");
589
+ created.push(".wiki-schema.yaml");
590
+ const indexPath = join6(vaultPath, "index.md");
591
+ if (!existsSync5(indexPath)) {
592
+ writeFileSync4(indexPath, "# Wiki Index\n", "utf-8");
593
+ created.push("index.md");
594
+ }
595
+ const logPath = join6(vaultPath, "log.md");
596
+ if (!existsSync5(logPath)) {
597
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
598
+ const logContent = `# Wiki Log
599
+
600
+ ## [${today}] init | ${schema.name}
601
+ Vault initialized.
602
+ `;
603
+ writeFileSync4(logPath, logContent, "utf-8");
604
+ created.push("log.md");
605
+ }
606
+ return {
607
+ success: true,
608
+ message: `Vault initialized at ${vaultPath}`,
609
+ created
610
+ };
611
+ }
612
+
613
+ // src/tools/create-page.ts
614
+ async function handleCreatePage(input, wikiManager) {
615
+ return wikiManager.createPage(input.title, input.content, input.pageType);
616
+ }
617
+
618
+ // src/tools/read-page.ts
619
+ async function handleReadPage(input, wikiManager) {
620
+ return wikiManager.readPage(input);
621
+ }
622
+
623
+ // src/tools/update-page.ts
624
+ async function handleUpdatePage(input, wikiManager) {
625
+ return wikiManager.updatePage(input.path, input.content);
626
+ }
627
+
628
+ // src/tools/delete-page.ts
629
+ async function handleDeletePage(input, wikiManager) {
630
+ return wikiManager.deletePage(input.path);
631
+ }
632
+
633
+ // src/tools/ingest.ts
634
+ import { readFileSync as readFileSync6, existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
635
+ import { join as join7 } from "path";
636
+ async function handleIngest(input, vaultPath, schema) {
637
+ const absPath = join7(vaultPath, input.source_path);
638
+ if (!existsSync6(absPath)) {
639
+ throw new Error(`Source not found: ${input.source_path}`);
640
+ }
641
+ const content = readFileSync6(absPath, "utf-8");
642
+ const wikiDir = join7(vaultPath, schema.paths.wiki);
643
+ let existingPages = [];
644
+ if (existsSync6(wikiDir)) {
645
+ existingPages = readdirSync3(wikiDir).filter((f) => f.endsWith(".md")).map((f) => `${schema.paths.wiki}/${f}`);
646
+ }
647
+ return {
648
+ content,
649
+ existing_pages: existingPages,
650
+ schema: {
651
+ pageTypes: schema.pageTypes,
652
+ linkStyle: schema.linkStyle
653
+ }
654
+ };
655
+ }
656
+
657
+ // src/tools/search.ts
658
+ async function handleSearch(input, searchProvider, wikiDir) {
659
+ const maxResults = input.max_results ?? 10;
660
+ const results = await searchProvider.search(input.query, {
661
+ maxResults,
662
+ wikiDir
663
+ });
664
+ return { results };
665
+ }
666
+
667
+ // src/tools/lint.ts
668
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
669
+ import { join as join8 } from "path";
670
+ import matter3 from "gray-matter";
671
+ async function handleLint(input, vaultPath, schema, linkResolver) {
672
+ const wikiDir = join8(vaultPath, schema.paths.wiki);
673
+ if (!existsSync7(wikiDir)) {
674
+ return {
675
+ orphan_pages: [],
676
+ broken_links: [],
677
+ missing_pages: [],
678
+ pages_without_frontmatter: [],
679
+ stale_index_entries: []
680
+ };
681
+ }
682
+ const files = readdirSync4(wikiDir).filter((f) => f.endsWith(".md")).map((f) => `${schema.paths.wiki}/${f}`);
683
+ const titleToPath = /* @__PURE__ */ new Map();
684
+ const pathToTitle = /* @__PURE__ */ new Map();
685
+ const inboundLinks = /* @__PURE__ */ new Set();
686
+ const brokenLinks = [];
687
+ const pagesWithoutFrontmatter = [];
688
+ for (const file of files) {
689
+ const absPath = join8(vaultPath, file);
690
+ const raw = readFileSync7(absPath, "utf-8");
691
+ const parsed = matter3(raw);
692
+ const title = parsed.data.title ?? file.replace(/.*\//, "").replace(".md", "");
693
+ titleToPath.set(title, file);
694
+ pathToTitle.set(file, title);
695
+ if (Object.keys(parsed.data).length === 0) {
696
+ pagesWithoutFrontmatter.push(file);
697
+ }
698
+ const links = linkResolver.parseLinks(raw);
699
+ for (const link of links) {
700
+ inboundLinks.add(link.target);
701
+ }
702
+ }
703
+ const orphanPages = [];
704
+ const missingPagesSet = /* @__PURE__ */ new Set();
705
+ for (const file of files) {
706
+ const title = pathToTitle.get(file);
707
+ if (!inboundLinks.has(title)) {
708
+ orphanPages.push(file);
709
+ }
710
+ }
711
+ for (const file of files) {
712
+ const absPath = join8(vaultPath, file);
713
+ const raw = readFileSync7(absPath, "utf-8");
714
+ const links = linkResolver.parseLinks(raw);
715
+ for (const link of links) {
716
+ if (!titleToPath.has(link.target)) {
717
+ brokenLinks.push({ from: file, to: link.target });
718
+ missingPagesSet.add(link.target);
719
+ }
720
+ }
721
+ }
722
+ const staleIndexEntries = [];
723
+ const indexPath = join8(vaultPath, "index.md");
724
+ if (existsSync7(indexPath)) {
725
+ const indexContent = readFileSync7(indexPath, "utf-8");
726
+ const indexLinks = linkResolver.parseLinks(indexContent);
727
+ for (const link of indexLinks) {
728
+ if (!titleToPath.has(link.target)) {
729
+ staleIndexEntries.push(link.target);
730
+ }
731
+ }
732
+ }
733
+ return {
734
+ orphan_pages: orphanPages,
735
+ broken_links: brokenLinks,
736
+ missing_pages: Array.from(missingPagesSet),
737
+ pages_without_frontmatter: pagesWithoutFrontmatter,
738
+ stale_index_entries: staleIndexEntries
739
+ };
740
+ }
741
+
742
+ // src/tools/index.ts
743
+ function registerTools(server, services) {
744
+ const { wikiManager, linkResolver, searchProvider, schema, vaultPath } = services;
745
+ const wikiDir = `${vaultPath}/${schema.paths.wiki}`;
746
+ server.registerTool(
747
+ "wiki_init",
748
+ {
749
+ title: "Initialize Wiki Vault",
750
+ description: "Create a new wiki vault with folder structure and default config",
751
+ inputSchema: z.object({
752
+ path: z.string().describe("Absolute path for the new vault"),
753
+ name: z.string().optional().describe("Wiki name"),
754
+ linkStyle: z.enum(["wikilink", "markdown"]).optional().describe("Link format")
755
+ })
756
+ },
757
+ async ({ path, name, linkStyle }) => {
758
+ const result = await handleInit({ path, name, linkStyle });
759
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
760
+ }
761
+ );
762
+ server.registerTool(
763
+ "wiki_ingest",
764
+ {
765
+ title: "Ingest Source",
766
+ description: "Read a raw source document and return its content with context for wiki processing",
767
+ inputSchema: z.object({
768
+ source_path: z.string().describe("Path to source file relative to vault root, e.g. raw/article.md")
769
+ })
770
+ },
771
+ async ({ source_path }) => {
772
+ const result = await handleIngest({ source_path }, vaultPath, schema);
773
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
774
+ }
775
+ );
776
+ server.registerTool(
777
+ "wiki_create_page",
778
+ {
779
+ title: "Create Wiki Page",
780
+ description: "Create a new wiki page with title, content (including frontmatter), and optional page type",
781
+ inputSchema: z.object({
782
+ title: z.string().describe("Page title"),
783
+ content: z.string().describe("Full markdown content including YAML frontmatter"),
784
+ pageType: z.string().optional().describe("Page type: source, concept, entity, or comparison")
785
+ })
786
+ },
787
+ async ({ title, content, pageType }) => {
788
+ const result = await handleCreatePage({ title, content, pageType }, wikiManager);
789
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
790
+ }
791
+ );
792
+ server.registerTool(
793
+ "wiki_read_page",
794
+ {
795
+ title: "Read Wiki Page",
796
+ description: "Read a wiki page by title or path",
797
+ inputSchema: z.object({
798
+ title: z.string().optional().describe("Page title to look up"),
799
+ path: z.string().optional().describe("Direct path to the page, e.g. wiki/page.md")
800
+ })
801
+ },
802
+ async ({ title, path }) => {
803
+ try {
804
+ const result = await handleReadPage({ title, path }, wikiManager);
805
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
806
+ } catch (err) {
807
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
808
+ }
809
+ }
810
+ );
811
+ server.registerTool(
812
+ "wiki_update_page",
813
+ {
814
+ title: "Update Wiki Page",
815
+ description: "Update an existing wiki page with new content",
816
+ inputSchema: z.object({
817
+ path: z.string().describe("Path to the page, e.g. wiki/page.md"),
818
+ content: z.string().describe("Full new markdown content including frontmatter")
819
+ })
820
+ },
821
+ async ({ path, content }) => {
822
+ const result = await handleUpdatePage({ path, content }, wikiManager);
823
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
824
+ }
825
+ );
826
+ server.registerTool(
827
+ "wiki_delete_page",
828
+ {
829
+ title: "Delete Wiki Page",
830
+ description: "Delete a wiki page and report any broken links",
831
+ inputSchema: z.object({
832
+ path: z.string().describe("Path to the page to delete, e.g. wiki/page.md")
833
+ })
834
+ },
835
+ async ({ path }) => {
836
+ const result = await handleDeletePage({ path }, wikiManager);
837
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
838
+ }
839
+ );
840
+ server.registerTool(
841
+ "wiki_search",
842
+ {
843
+ title: "Search Wiki",
844
+ description: "Search across wiki pages using text or semantic search",
845
+ inputSchema: z.object({
846
+ query: z.string().describe("Search query"),
847
+ max_results: z.number().optional().describe("Maximum results to return (default: 10)")
848
+ })
849
+ },
850
+ async ({ query, max_results }) => {
851
+ const result = await handleSearch({ query, max_results }, searchProvider, wikiDir);
852
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
853
+ }
854
+ );
855
+ server.registerTool(
856
+ "wiki_lint",
857
+ {
858
+ title: "Lint Wiki",
859
+ description: "Health-check the wiki: find orphan pages, broken links, missing frontmatter, stale index entries",
860
+ inputSchema: z.object({
861
+ scope: z.enum(["full", "recent"]).optional().describe("Scan scope: full or recent")
862
+ })
863
+ },
864
+ async ({ scope }) => {
865
+ const result = await handleLint({ scope }, vaultPath, schema, linkResolver);
866
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
867
+ }
868
+ );
869
+ }
870
+
871
+ // src/server.ts
872
+ async function createServer(config) {
873
+ const { vaultPath } = config;
874
+ const schema = loadSchema(vaultPath);
875
+ const linkResolver = new LinkResolver(schema.linkStyle, schema.paths.wiki);
876
+ const logManager = new LogManager(vaultPath);
877
+ const indexManager = new IndexManager(vaultPath, linkResolver);
878
+ const wikiManager = new WikiManager(vaultPath, schema, linkResolver, indexManager, logManager);
879
+ const searchProvider = await detectSearchProvider();
880
+ const server = new McpServer({
881
+ name: "llm-wiki-mcp",
882
+ version: "0.1.0"
883
+ });
884
+ registerTools(server, {
885
+ wikiManager,
886
+ linkResolver,
887
+ searchProvider,
888
+ schema,
889
+ vaultPath
890
+ });
891
+ return server;
892
+ }
893
+ export {
894
+ createServer
895
+ };