@hanna84/mcp-writing 2.14.0 → 2.15.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/CHANGELOG.md CHANGED
@@ -4,11 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [v2.15.0](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v2.14.0...v2.15.0)
9
+
10
+ - feat(reference): add reference link query tools [`#148`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/148)
12
+
7
13
  #### [v2.14.0](https://github.com/hannasdev/mcp-writing.git
8
14
  /compare/v2.13.0...v2.14.0)
9
15
 
16
+ > 30 April 2026
17
+
10
18
  - feat(reference): add reference link indexing for Phase 4B [`#147`](https://github.com/hannasdev/mcp-writing.git
11
19
  /pull/147)
20
+ - Release 2.14.0 [`0b82946`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/0b829469448cb86008113dbf1b05f92e700b61a1)
12
22
 
13
23
  #### [v2.13.0](https://github.com/hannasdev/mcp-writing.git
14
24
  /compare/v2.12.22...v2.13.0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "homepage": "https://hannasdev.github.io/mcp-writing/",
6
6
  "type": "module",
@@ -485,6 +485,160 @@ export function registerSearchTools(s, {
485
485
  }
486
486
  );
487
487
 
488
+ // ---- list_scene_references -----------------------------------------------
489
+ s.tool(
490
+ "list_scene_references",
491
+ "List direct reference documents linked from a scene via metadata (for example, reference_ids). Returns only one-hop scene -> reference links and does not recursively traverse related references. If scene IDs are reused across projects, omitting project_id returns CONFLICT with candidate project_ids.",
492
+ {
493
+ scene_id: z.string().describe("Scene ID to inspect."),
494
+ project_id: z.string().optional().describe("Optional project ID to disambiguate duplicate scene IDs across projects."),
495
+ },
496
+ async ({ scene_id, project_id }) => {
497
+ let scene;
498
+ if (project_id) {
499
+ scene = db.prepare(`
500
+ SELECT scene_id, project_id
501
+ FROM scenes
502
+ WHERE scene_id = ? AND project_id = ?
503
+ LIMIT 1
504
+ `).get(scene_id, project_id);
505
+ if (!scene) {
506
+ return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'.`);
507
+ }
508
+ } else {
509
+ const matches = db.prepare(`
510
+ SELECT scene_id, project_id
511
+ FROM scenes
512
+ WHERE scene_id = ?
513
+ ORDER BY project_id
514
+ `).all(scene_id);
515
+ if (matches.length === 0) {
516
+ return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found.`);
517
+ }
518
+ if (matches.length > 1) {
519
+ return errorResponse(
520
+ "CONFLICT",
521
+ `Scene ID '${scene_id}' exists in multiple projects. Provide project_id to disambiguate.`,
522
+ { scene_id, project_ids: matches.map(row => row.project_id) }
523
+ );
524
+ }
525
+ scene = matches[0];
526
+ }
527
+
528
+ const links = db.prepare(`
529
+ SELECT
530
+ rl.target_doc_id,
531
+ rl.relation,
532
+ rd.project_id AS target_project_id,
533
+ rd.universe_id AS target_universe_id,
534
+ rd.type,
535
+ rd.title,
536
+ rd.summary,
537
+ rd.file_path
538
+ FROM reference_links rl
539
+ LEFT JOIN reference_docs rd ON rd.doc_id = rl.target_doc_id
540
+ WHERE rl.source_kind = 'scene' AND rl.source_project_id = ? AND rl.source_id = ?
541
+ ORDER BY rl.target_doc_id
542
+ `).all(scene.project_id ?? "", scene.scene_id);
543
+
544
+ if (links.length === 0) {
545
+ return errorResponse("NO_RESULTS", `No reference links found for scene '${scene.scene_id}' in project '${scene.project_id}'.`);
546
+ }
547
+
548
+ const tagsStmt = db.prepare(`
549
+ SELECT tag
550
+ FROM reference_doc_tags
551
+ WHERE doc_id = ?
552
+ ORDER BY tag
553
+ `);
554
+ const references = links.map((row) => ({
555
+ doc_id: row.target_doc_id,
556
+ relation: row.relation,
557
+ project_id: row.target_project_id,
558
+ universe_id: row.target_universe_id,
559
+ type: row.type,
560
+ title: row.title,
561
+ summary: row.summary,
562
+ file_path: row.file_path,
563
+ tags: tagsStmt.all(row.target_doc_id).map(tagRow => tagRow.tag),
564
+ }));
565
+
566
+ return {
567
+ content: [{
568
+ type: "text",
569
+ text: JSON.stringify({
570
+ scene_id: scene.scene_id,
571
+ project_id: scene.project_id,
572
+ references,
573
+ }, null, 2),
574
+ }],
575
+ };
576
+ }
577
+ );
578
+
579
+ // ---- get_reference_doc ----------------------------------------------------
580
+ s.tool(
581
+ "get_reference_doc",
582
+ "Get metadata for a reference document by doc_id. Optionally includes exactly one hop of related reference docs.",
583
+ {
584
+ doc_id: z.string().describe("Reference document ID."),
585
+ include_related: z.boolean().optional().describe("If true, include one-hop related reference docs."),
586
+ },
587
+ async ({ doc_id, include_related = false }) => {
588
+ const doc = db.prepare(`
589
+ SELECT doc_id, project_id, universe_id, type, title, summary, file_path
590
+ FROM reference_docs
591
+ WHERE doc_id = ?
592
+ `).get(doc_id);
593
+ if (!doc) {
594
+ return errorResponse("NOT_FOUND", `Reference document '${doc_id}' not found.`);
595
+ }
596
+
597
+ const tagsStmt = db.prepare(`
598
+ SELECT tag
599
+ FROM reference_doc_tags
600
+ WHERE doc_id = ?
601
+ ORDER BY tag
602
+ `);
603
+ const payload = {
604
+ ...doc,
605
+ tags: tagsStmt.all(doc.doc_id).map(row => row.tag),
606
+ };
607
+
608
+ if (include_related) {
609
+ const relatedRows = db.prepare(`
610
+ SELECT
611
+ rl.target_doc_id,
612
+ rl.relation,
613
+ rd.project_id,
614
+ rd.universe_id,
615
+ rd.type,
616
+ rd.title,
617
+ rd.summary,
618
+ rd.file_path
619
+ FROM reference_links rl
620
+ LEFT JOIN reference_docs rd ON rd.doc_id = rl.target_doc_id
621
+ WHERE rl.source_kind = 'reference' AND rl.source_project_id = ? AND rl.source_id = ?
622
+ ORDER BY rl.target_doc_id
623
+ `).all(doc.project_id ?? "", doc.doc_id);
624
+
625
+ payload.related = relatedRows.map((row) => ({
626
+ doc_id: row.target_doc_id,
627
+ relation: row.relation,
628
+ project_id: row.project_id,
629
+ universe_id: row.universe_id,
630
+ type: row.type,
631
+ title: row.title,
632
+ summary: row.summary,
633
+ file_path: row.file_path,
634
+ tags: tagsStmt.all(row.target_doc_id).map(tagRow => tagRow.tag),
635
+ }));
636
+ }
637
+
638
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
639
+ }
640
+ );
641
+
488
642
  // ---- list_threads --------------------------------------------------------
489
643
  s.tool(
490
644
  "list_threads",