agent-method 1.5.12 → 1.5.15

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/lib/mcp-server.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * Uses @modelcontextprotocol/sdk McpServer with stdio transport.
15
15
  */
16
16
 
17
- import { readFileSync, statSync } from "node:fs";
17
+ import { readFileSync, statSync, existsSync } from "node:fs";
18
18
  import { resolve, join } from "node:path";
19
19
  import { z } from "zod";
20
20
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -29,7 +29,25 @@ import {
29
29
  resolveCascade,
30
30
  validateEntryPoint,
31
31
  detectProjectType,
32
+ routeByQueryType,
32
33
  } from "./pipeline.js";
34
+ import { loadDocGraph, resolveDocsMapPath } from "./boundaries.js";
35
+ import { orderByDependencies } from "./dependencies.js";
36
+
37
+ // Shared CLI helpers for reusing status/scan logic without shelling out
38
+ import {
39
+ findEntryPoint,
40
+ readMethodVersion,
41
+ basename_of,
42
+ pkg,
43
+ getPipeline,
44
+ loadRegistryData,
45
+ } from "./cli/helpers.js";
46
+ import { runCasestudy } from "./cli/casestudy.js";
47
+ import { initProject } from "./init.js";
48
+ import { runClose } from "./cli/close.js";
49
+ import { runAdd } from "./cli/add.js";
50
+ import { runRecord } from "./cli/record.js";
33
51
 
34
52
  // ---------------------------------------------------------------------------
35
53
  // Registry singleton (loaded once at startup)
@@ -230,6 +248,314 @@ export function createServer(registryPath) {
230
248
  }
231
249
  );
232
250
 
251
+ // -------------------------------------------------------------------------
252
+ // Helper: parse DOCS-MAP.md for docs_coverage (mirrors cli/docs.js)
253
+ // -------------------------------------------------------------------------
254
+
255
+ function parseDocsMapForMcp(content, projectDir) {
256
+ const inventory = [];
257
+ const mappings = [];
258
+ const scaffolding = [];
259
+
260
+ // Docs inventory
261
+ const invSection = content.match(
262
+ /## Docs inventory\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
263
+ );
264
+ if (invSection) {
265
+ for (const row of invSection[1].trim().split("\n")) {
266
+ const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
267
+ if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
268
+ const path = cols[0];
269
+ const exists = existsSync(join(projectDir, path));
270
+ inventory.push({
271
+ path,
272
+ purpose: cols[1],
273
+ sources: cols[2],
274
+ status: cols[3] || (exists ? "active" : "missing"),
275
+ exists,
276
+ });
277
+ }
278
+ }
279
+ }
280
+
281
+ // Component-to-docs mapping
282
+ const mapSection = content.match(
283
+ /## Component-to-docs mapping\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
284
+ );
285
+ if (mapSection) {
286
+ for (const row of mapSection[1].trim().split("\n")) {
287
+ const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
288
+ if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
289
+ mappings.push({
290
+ component: cols[0],
291
+ documentedIn: cols[1],
292
+ trigger: cols[2],
293
+ });
294
+ }
295
+ }
296
+ }
297
+
298
+ // Scaffolding rules
299
+ const scaffSection = content.match(
300
+ /## Scaffolding rules\s*\n[\s\S]*?\n\|[^\n]+\n\|[-| :]+\n((?:\|[^\n]+\n)*)/
301
+ );
302
+ if (scaffSection) {
303
+ for (const row of scaffSection[1].trim().split("\n")) {
304
+ const cols = row.split("|").map((c) => c.trim()).filter((c) => c);
305
+ if (cols.length >= 3 && !cols[0].startsWith("<!--")) {
306
+ scaffolding.push({
307
+ condition: cols[0],
308
+ proposedDoc: cols[1],
309
+ seedFrom: cols[2],
310
+ });
311
+ }
312
+ }
313
+ }
314
+
315
+ const existingDocs = inventory.filter((i) => i.exists).map((i) => i.path);
316
+ const missingDocs = inventory.filter((i) => !i.exists).map((i) => i.path);
317
+
318
+ // Docs on disk but not in inventory (simple heuristic)
319
+ const docsDir = join(projectDir, "docs");
320
+ const unmappedDocs = [];
321
+ if (existsSync(docsDir)) {
322
+ const inventoryPaths = new Set(inventory.map((i) => i.path));
323
+ const candidates = ["docs/index.md"];
324
+ for (const c of candidates) {
325
+ if (existsSync(join(projectDir, c)) && !inventoryPaths.has(c)) {
326
+ unmappedDocs.push(c);
327
+ }
328
+ }
329
+ }
330
+
331
+ const summary = {
332
+ totalDocs: inventory.length,
333
+ existingDocs: existingDocs.length,
334
+ missingDocs: missingDocs.length,
335
+ unmappedDocs: unmappedDocs.length,
336
+ mappedComponents: mappings.length,
337
+ scaffoldingRules: scaffolding.length,
338
+ };
339
+
340
+ return {
341
+ inventory,
342
+ mappings,
343
+ scaffolding,
344
+ summary,
345
+ existingDocs,
346
+ missingDocs,
347
+ unmappedDocs,
348
+ };
349
+ }
350
+
351
+ // -------------------------------------------------------------------------
352
+ // Tool 1d: docs_coverage — docs coverage + dependency summary (wwa docs --json)
353
+ // -------------------------------------------------------------------------
354
+
355
+ server.tool(
356
+ "docs_coverage",
357
+ "Show docs coverage report from .context/DOCS-MAP.md and doc-tokens.yaml (MCP equivalent of `wwa docs --json`).",
358
+ {
359
+ directory: z
360
+ .string()
361
+ .default(".")
362
+ .describe("Project directory containing .context/DOCS-MAP.md"),
363
+ },
364
+ async ({ directory }) => {
365
+ const dir = resolve(directory || ".");
366
+ const docsMapPath = await resolveDocsMapPath(dir);
367
+
368
+ if (!existsSync(docsMapPath)) {
369
+ return {
370
+ content: [
371
+ {
372
+ type: "text",
373
+ text: JSON.stringify(
374
+ {
375
+ directory: dir,
376
+ error: "No .context/DOCS-MAP.md found",
377
+ },
378
+ null,
379
+ 2
380
+ ),
381
+ },
382
+ ],
383
+ };
384
+ }
385
+
386
+ const contentStr = readFileSync(docsMapPath, "utf-8");
387
+ const report = parseDocsMapForMcp(contentStr, dir);
388
+
389
+ // Enrich with dependency graph summary when available
390
+ const docGraph = await loadDocGraph(dir);
391
+ if (docGraph && docGraph.nodes && docGraph.edges) {
392
+ const nodes = docGraph.nodes.length;
393
+ const edges = docGraph.edges.length;
394
+ const edgesByType = {};
395
+ for (const e of docGraph.edges) {
396
+ edgesByType[e.type] = (edgesByType[e.type] || 0) + 1;
397
+ }
398
+ // Orphans: docs with no edges
399
+ const connected = new Set();
400
+ for (const e of docGraph.edges) {
401
+ connected.add(e.from);
402
+ connected.add(e.to);
403
+ }
404
+ const orphans = docGraph.nodes
405
+ .filter((n) => !connected.has(n.path))
406
+ .map((n) => n.path);
407
+
408
+ report.dependencyGraph = {
409
+ nodeCount: nodes,
410
+ edgeCount: edges,
411
+ edgesByType,
412
+ orphanCount: orphans.length,
413
+ orphans,
414
+ lastScan: docGraph.last_scan || null,
415
+ defaultsApplied: docGraph.defaults_applied || null,
416
+ };
417
+ }
418
+
419
+ return {
420
+ content: [
421
+ {
422
+ type: "text",
423
+ text: JSON.stringify(
424
+ {
425
+ directory: dir,
426
+ docsMapPath,
427
+ ...report,
428
+ },
429
+ null,
430
+ 2
431
+ ),
432
+ },
433
+ ],
434
+ };
435
+ }
436
+ );
437
+ // -------------------------------------------------------------------------
438
+ // Tool 1b: status — methodology version status (wwa status)
439
+ // -------------------------------------------------------------------------
440
+
441
+ server.tool(
442
+ "status",
443
+ "Check if the project entry point method_version matches the installed wwa version (MCP equivalent of `wwa status`).",
444
+ {
445
+ directory: z
446
+ .string()
447
+ .default(".")
448
+ .describe("Project directory to scan for entry point"),
449
+ },
450
+ async ({ directory }) => {
451
+ const dir = resolve(directory || ".");
452
+ const ep = findEntryPoint(dir);
453
+ if (!ep) {
454
+ const msg =
455
+ `No entry point found in ${dir} (looked for CLAUDE.md, .cursorrules, AGENT.md).`;
456
+ return {
457
+ content: [
458
+ {
459
+ type: "text",
460
+ text: JSON.stringify(
461
+ {
462
+ directory: dir,
463
+ status: "no_entry_point",
464
+ error: msg,
465
+ },
466
+ null,
467
+ 2
468
+ ),
469
+ },
470
+ ],
471
+ };
472
+ }
473
+
474
+ const epVersion = readMethodVersion(ep);
475
+ const installed = pkg.version;
476
+ const installedMajorMinor = installed.split(".").slice(0, 2).join(".");
477
+
478
+ let verStatus;
479
+ let message;
480
+ if (!epVersion) {
481
+ verStatus = "no_version";
482
+ message =
483
+ `Entry point ${basename_of(ep)} has no method_version setting. ` +
484
+ `Add 'method_version: ${installedMajorMinor}' to enable version tracking.`;
485
+ } else if (epVersion === installedMajorMinor) {
486
+ verStatus = "current";
487
+ message =
488
+ `Entry point ${basename_of(ep)} is current ` +
489
+ `(method_version: ${epVersion}, installed: ${installed}).`;
490
+ } else {
491
+ verStatus = "outdated";
492
+ message =
493
+ `Entry point ${basename_of(ep)} is outdated ` +
494
+ `(method_version: ${epVersion}, installed: ${installed}). ` +
495
+ `Run \`wwa upgrade ${dir}\` to update.`;
496
+ }
497
+
498
+ const result = {
499
+ directory: dir,
500
+ entry_point: ep,
501
+ entry_point_version: epVersion,
502
+ installed_version: installed,
503
+ status: verStatus,
504
+ message,
505
+ };
506
+
507
+ return {
508
+ content: [
509
+ {
510
+ type: "text",
511
+ text: JSON.stringify(result, null, 2),
512
+ },
513
+ ],
514
+ };
515
+ }
516
+ );
517
+
518
+ // -------------------------------------------------------------------------
519
+ // Tool 1c: scan_project — project type detection (wwa scan --json, simplified)
520
+ // -------------------------------------------------------------------------
521
+
522
+ server.tool(
523
+ "scan_project",
524
+ "Detect project type from directory contents (MCP equivalent of `wwa scan --json`, without side effects).",
525
+ {
526
+ directory: z
527
+ .string()
528
+ .default(".")
529
+ .describe("Directory to analyze"),
530
+ },
531
+ async ({ directory }) => {
532
+ const dir = resolve(directory || ".");
533
+ const result = detectProjectType(dir);
534
+
535
+ const project_type = result.project_type || "general";
536
+ const friendlyMap = { analytical: "context", mixed: "mix" };
537
+ const friendly = friendlyMap[project_type] || project_type;
538
+ const init_command = `wwa init ${friendly}`;
539
+
540
+ const output = {
541
+ directory: dir,
542
+ project_type,
543
+ confidence: result.confidence || "unknown",
544
+ signals: result.signals || [],
545
+ suggested_init: init_command,
546
+ };
547
+
548
+ return {
549
+ content: [
550
+ {
551
+ type: "text",
552
+ text: JSON.stringify(output, null, 2),
553
+ },
554
+ ],
555
+ };
556
+ }
557
+ );
558
+
233
559
  // -------------------------------------------------------------------------
234
560
  // Helper: list_cli_commands — expose CLI command metadata to agents
235
561
  // -------------------------------------------------------------------------
@@ -381,6 +707,521 @@ export function createServer(registryPath) {
381
707
  }
382
708
  );
383
709
 
710
+ // -------------------------------------------------------------------------
711
+ // Helper: route_by_query_type_for_mcp — shared by routable workflow tools
712
+ // -------------------------------------------------------------------------
713
+
714
+ async function routeByQueryTypeForMcp({
715
+ queryType,
716
+ projectType,
717
+ stage,
718
+ firstSession,
719
+ registryPath,
720
+ directory,
721
+ }) {
722
+ const regData = await loadRegistryData(registryPath);
723
+ const dir = resolve(directory || ".");
724
+
725
+ const result = routeByQueryType(
726
+ queryType,
727
+ projectType,
728
+ stage,
729
+ regData,
730
+ firstSession || false
731
+ );
732
+
733
+ const docGraph = await loadDocGraph(dir);
734
+ if (docGraph && result.S4_compute?.read_set) {
735
+ const orderedReads = orderByDependencies(result.S4_compute.read_set, docGraph);
736
+ result.S4b_order = {
737
+ ordered_reads: orderedReads,
738
+ ordering_source: "doc_graph",
739
+ };
740
+ }
741
+
742
+ return {
743
+ directory: dir,
744
+ query_type: queryType,
745
+ project_type: projectType,
746
+ stage,
747
+ first_session: !!firstSession,
748
+ S1_classify: result.S1_classify || null,
749
+ S2_select: result.S2_select || null,
750
+ S3_resolve: result.S3_resolve || null,
751
+ S4_compute: result.S4_compute || null,
752
+ S4b_order: result.S4b_order || null,
753
+ };
754
+ }
755
+
756
+ // -------------------------------------------------------------------------
757
+ // Routable workflow tools (Phase 7p)
758
+ // -------------------------------------------------------------------------
759
+
760
+ const ROUTABLE_MCP_TOOLS = [
761
+ {
762
+ name: "project_discovery_route",
763
+ queryType: "project_discovery",
764
+ description:
765
+ "Route a project discovery workflow (MCP equivalent of `wwa project-discovery --json`).",
766
+ },
767
+ {
768
+ name: "context_refresh_route",
769
+ queryType: "context_refresh",
770
+ description:
771
+ "Route a context refresh workflow (MCP equivalent of `wwa context-refresh --json`).",
772
+ },
773
+ {
774
+ name: "phase_complete_route",
775
+ queryType: "phase_completion",
776
+ description:
777
+ "Route a phase completion workflow (MCP equivalent of `wwa phase-complete --json`).",
778
+ },
779
+ {
780
+ name: "backlog_route",
781
+ queryType: "backlog",
782
+ description:
783
+ "Route a backlog workflow (MCP equivalent of `wwa backlog --json`).",
784
+ },
785
+ {
786
+ name: "plan_create_route",
787
+ queryType: "planning",
788
+ description:
789
+ "Route a planning workflow (MCP equivalent of `wwa plan-create --json`).",
790
+ },
791
+ {
792
+ name: "dependency_analysis_route",
793
+ queryType: "dependency_analysis",
794
+ description:
795
+ "Route a dependency analysis workflow (MCP equivalent of `wwa dependency-analysis --json`).",
796
+ },
797
+ {
798
+ name: "debt_assessment_route",
799
+ queryType: "debt_assessment",
800
+ description:
801
+ "Route a debt assessment workflow (MCP equivalent of `wwa debt-assessment --json`).",
802
+ },
803
+ {
804
+ name: "docs_update_route",
805
+ queryType: "docs_update",
806
+ description:
807
+ "Route a docs update workflow (MCP equivalent of `wwa docs-update --json`).",
808
+ },
809
+ {
810
+ name: "cross_reference_route",
811
+ queryType: "cross_reference",
812
+ description:
813
+ "Route a cross-reference workflow (MCP equivalent of `wwa cross-reference --json`).",
814
+ },
815
+ ];
816
+
817
+ for (const { name, queryType, description } of ROUTABLE_MCP_TOOLS) {
818
+ server.tool(
819
+ name,
820
+ description,
821
+ {
822
+ project_type: z
823
+ .enum(["code", "data", "analytical", "mixed", "general"])
824
+ .default("general")
825
+ .describe(
826
+ "Project type: code, data, analytical/context, mixed, general (matches CLI project-type semantics)"
827
+ ),
828
+ stage: z
829
+ .string()
830
+ .default("scope")
831
+ .describe("Workflow stage: scope, act, verify (see registry workflows)"),
832
+ first_session: z
833
+ .boolean()
834
+ .default(false)
835
+ .describe("Force WF-04 bootstrap behavior (first-session workflows)"),
836
+ registry_path: z
837
+ .string()
838
+ .optional()
839
+ .describe(
840
+ "Optional path to feature-registry.yaml (defaults to discovery from project root)"
841
+ ),
842
+ directory: z
843
+ .string()
844
+ .default(".")
845
+ .describe(
846
+ "Project directory (used for dependency graph ordering and registry discovery)"
847
+ ),
848
+ },
849
+ async ({ project_type, stage, first_session, registry_path, directory }) => {
850
+ const routed = await routeByQueryTypeForMcp({
851
+ queryType,
852
+ projectType: project_type,
853
+ stage,
854
+ firstSession: first_session,
855
+ registryPath: registry_path,
856
+ directory,
857
+ });
858
+
859
+ return {
860
+ content: [
861
+ {
862
+ type: "text",
863
+ text: JSON.stringify(routed, null, 2),
864
+ },
865
+ ],
866
+ };
867
+ }
868
+ );
869
+ }
870
+
871
+ // -------------------------------------------------------------------------
872
+ // Tool (write-safe): casestudy_generate — generate case study artifacts
873
+ // -------------------------------------------------------------------------
874
+
875
+ server.tool(
876
+ "casestudy_generate",
877
+ "Generate a structured case study from project methodology files (write-safe MCP analog of `wwa casestudy`). Writes only under case-studies/ using safeWriteFile.",
878
+ {
879
+ directory: z
880
+ .string()
881
+ .default(".")
882
+ .describe("Project directory to analyze"),
883
+ name: z
884
+ .string()
885
+ .optional()
886
+ .describe("Optional project name override"),
887
+ yaml_only: z
888
+ .boolean()
889
+ .default(false)
890
+ .describe("If true, write only a .yaml case study (no markdown)"),
891
+ output: z
892
+ .string()
893
+ .optional()
894
+ .describe(
895
+ "Optional explicit output path for the primary artifact (YAML or markdown). Default is case-studies/<slug>.{md,yaml}"
896
+ ),
897
+ internal_registry: z
898
+ .boolean()
899
+ .default(false)
900
+ .describe("Include docs/internal/doc-registry.yaml data when available"),
901
+ },
902
+ async ({ directory, name, yaml_only, output, internal_registry }) => {
903
+ const dir = resolve(directory || ".");
904
+ try {
905
+ const result = await runCasestudy(dir, {
906
+ name,
907
+ yamlOnly: yaml_only,
908
+ output,
909
+ internalRegistry: internal_registry,
910
+ });
911
+
912
+ return {
913
+ content: [
914
+ {
915
+ type: "text",
916
+ text: JSON.stringify(
917
+ {
918
+ directory: dir,
919
+ mode: result.mode,
920
+ markdown_path: result.markdownPath || null,
921
+ yaml_path: result.yamlPath || null,
922
+ sessions: result.sessions,
923
+ warnings: result.warnings || [],
924
+ },
925
+ null,
926
+ 2
927
+ ),
928
+ },
929
+ ],
930
+ };
931
+ } catch (e) {
932
+ return {
933
+ content: [
934
+ {
935
+ type: "text",
936
+ text: JSON.stringify(
937
+ {
938
+ directory: dir,
939
+ error: e.message || "Failed to generate case study",
940
+ details: e.details || null,
941
+ },
942
+ null,
943
+ 2
944
+ ),
945
+ },
946
+ ],
947
+ };
948
+ }
949
+ }
950
+ );
951
+
952
+ // -------------------------------------------------------------------------
953
+ // Tool (write-safe): init_project — initialize methodology templates
954
+ // -------------------------------------------------------------------------
955
+
956
+ server.tool(
957
+ "init_project",
958
+ "Initialize a project with wwa methodology templates (write-safe analog of `wwa init` for methodology files). Writes only methodology artifacts plus entry points.",
959
+ {
960
+ project_type: z
961
+ .enum(["code", "data", "analytical", "mixed", "general"])
962
+ .describe("Project type to initialize"),
963
+ directory: z
964
+ .string()
965
+ .default(".")
966
+ .describe("Target project directory"),
967
+ tier: z
968
+ .enum(["starter", "full"])
969
+ .default("starter")
970
+ .describe("Template tier"),
971
+ runtime: z
972
+ .enum(["claude", "cursor", "all"])
973
+ .default("all")
974
+ .describe("Agent runtime (controls which entry points are kept)"),
975
+ profile: z
976
+ .enum(["lite", "standard", "full"])
977
+ .default("standard")
978
+ .describe("Integration profile"),
979
+ onboarding: z
980
+ .enum(["greenfield", "brownfield", "auto"])
981
+ .default("auto")
982
+ .describe("Onboarding scenario"),
983
+ registry_path: z
984
+ .string()
985
+ .optional()
986
+ .describe("Optional explicit path to feature-registry.yaml"),
987
+ },
988
+ async ({
989
+ project_type,
990
+ directory,
991
+ tier,
992
+ runtime,
993
+ profile,
994
+ onboarding,
995
+ registry_path,
996
+ }) => {
997
+ const dir = resolve(directory || ".");
998
+ try {
999
+ await initProject(project_type, dir, {
1000
+ tier,
1001
+ runtime,
1002
+ profile,
1003
+ onboarding,
1004
+ registryPath: registry_path,
1005
+ });
1006
+
1007
+ return {
1008
+ content: [
1009
+ {
1010
+ type: "text",
1011
+ text: JSON.stringify(
1012
+ {
1013
+ directory: dir,
1014
+ project_type,
1015
+ tier,
1016
+ runtime,
1017
+ profile,
1018
+ onboarding,
1019
+ ok: true,
1020
+ note:
1021
+ "Templates and methodology files created/merged. Inspect entry point and .context/ before first session.",
1022
+ },
1023
+ null,
1024
+ 2
1025
+ ),
1026
+ },
1027
+ ],
1028
+ };
1029
+ } catch (e) {
1030
+ return {
1031
+ content: [
1032
+ {
1033
+ type: "text",
1034
+ text: JSON.stringify(
1035
+ {
1036
+ directory: dir,
1037
+ project_type,
1038
+ ok: false,
1039
+ error: e.message || "Failed to initialize project",
1040
+ },
1041
+ null,
1042
+ 2
1043
+ ),
1044
+ },
1045
+ ],
1046
+ };
1047
+ }
1048
+ }
1049
+ );
1050
+
1051
+ // -------------------------------------------------------------------------
1052
+ // Tool (write-safe): close_session — run close checklist + write management
1053
+ // -------------------------------------------------------------------------
1054
+
1055
+ server.tool(
1056
+ "close_session",
1057
+ "Run session close checklist, update management artifacts, and compute project snapshot (write-safe analog of `wwa close`).",
1058
+ {
1059
+ directory: z
1060
+ .string()
1061
+ .default(".")
1062
+ .describe("Project directory"),
1063
+ internal_registry: z
1064
+ .boolean()
1065
+ .default(false)
1066
+ .describe(
1067
+ "Include docs/internal/doc-registry.yaml health when available (same as --internal-registry)"
1068
+ ),
1069
+ },
1070
+ async ({ directory, internal_registry }) => {
1071
+ const dir = resolve(directory || ".");
1072
+ try {
1073
+ const result = await runClose(dir, {
1074
+ internalRegistry: internal_registry,
1075
+ });
1076
+ return {
1077
+ content: [
1078
+ {
1079
+ type: "text",
1080
+ text: JSON.stringify(result, null, 2),
1081
+ },
1082
+ ],
1083
+ };
1084
+ } catch (e) {
1085
+ return {
1086
+ content: [
1087
+ {
1088
+ type: "text",
1089
+ text: JSON.stringify(
1090
+ {
1091
+ directory: dir,
1092
+ error: e.message || "wwa close failed",
1093
+ },
1094
+ null,
1095
+ 2
1096
+ ),
1097
+ },
1098
+ ],
1099
+ };
1100
+ }
1101
+ }
1102
+ );
1103
+
1104
+ // -------------------------------------------------------------------------
1105
+ // Tools (write-safe): add_* — append structured content via wwa add
1106
+ // -------------------------------------------------------------------------
1107
+
1108
+ const ADD_MCP_TOOLS = [
1109
+ { name: "add_backlog", type: "backlog" },
1110
+ { name: "add_decision", type: "decision" },
1111
+ { name: "add_finding", type: "finding" },
1112
+ { name: "add_session", type: "session" },
1113
+ { name: "add_summary", type: "summary" },
1114
+ ];
1115
+
1116
+ for (const { name, type } of ADD_MCP_TOOLS) {
1117
+ server.tool(
1118
+ name,
1119
+ `Append ${type} content to methodology files (write-safe analog of \`wwa add ${type}\`).`,
1120
+ {
1121
+ directory: z
1122
+ .string()
1123
+ .default(".")
1124
+ .describe("Project directory"),
1125
+ content: z
1126
+ .string()
1127
+ .describe(
1128
+ type === "decision"
1129
+ ? "Decision text; use 'Decision | Rationale' to include rationale"
1130
+ : "Content to append"
1131
+ ),
1132
+ dry_run: z
1133
+ .boolean()
1134
+ .default(false)
1135
+ .describe("If true, do not write — just return target and content"),
1136
+ },
1137
+ async ({ directory, content, dry_run }) => {
1138
+ const dir = resolve(directory || ".");
1139
+ try {
1140
+ const result = await runAdd(type, content, {
1141
+ directory: dir,
1142
+ dryRun: dry_run,
1143
+ });
1144
+ return {
1145
+ content: [
1146
+ {
1147
+ type: "text",
1148
+ text: JSON.stringify(result, null, 2),
1149
+ },
1150
+ ],
1151
+ };
1152
+ } catch (e) {
1153
+ return {
1154
+ content: [
1155
+ {
1156
+ type: "text",
1157
+ text: JSON.stringify(
1158
+ {
1159
+ directory: dir,
1160
+ type,
1161
+ error: e.message || "wwa add failed",
1162
+ code: e.code || null,
1163
+ },
1164
+ null,
1165
+ 2
1166
+ ),
1167
+ },
1168
+ ],
1169
+ };
1170
+ }
1171
+ }
1172
+ );
1173
+ }
1174
+
1175
+ // -------------------------------------------------------------------------
1176
+ // Tool (write-safe): record_last_cli_run — append last CLI run to SESSION-LOG
1177
+ // -------------------------------------------------------------------------
1178
+
1179
+ server.tool(
1180
+ "record_last_cli_run",
1181
+ "Record the last CLI run summary to SESSION-LOG.md (write-safe analog of `wwa record`).",
1182
+ {
1183
+ directory: z
1184
+ .string()
1185
+ .default(".")
1186
+ .describe("Project directory"),
1187
+ dry_run: z
1188
+ .boolean()
1189
+ .default(false)
1190
+ .describe("If true, do not write — just return target and block"),
1191
+ },
1192
+ async ({ directory, dry_run }) => {
1193
+ const dir = resolve(directory || ".");
1194
+ try {
1195
+ const result = await runRecord(dir, { dryRun: dry_run });
1196
+ return {
1197
+ content: [
1198
+ {
1199
+ type: "text",
1200
+ text: JSON.stringify(result, null, 2),
1201
+ },
1202
+ ],
1203
+ };
1204
+ } catch (e) {
1205
+ return {
1206
+ content: [
1207
+ {
1208
+ type: "text",
1209
+ text: JSON.stringify(
1210
+ {
1211
+ directory: dir,
1212
+ error: e.message || "wwa record failed",
1213
+ code: e.code || null,
1214
+ },
1215
+ null,
1216
+ 2
1217
+ ),
1218
+ },
1219
+ ],
1220
+ };
1221
+ }
1222
+ }
1223
+ );
1224
+
384
1225
  // -------------------------------------------------------------------------
385
1226
  // Tool 5: check_capability — model tier query
386
1227
  // -------------------------------------------------------------------------