@geraldmaron/construct 1.4.0 → 1.4.2

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.
Files changed (65) hide show
  1. package/bin/construct +2 -1
  2. package/bin/construct-postinstall.mjs +27 -2
  3. package/config/tag-vocabulary.json +264 -0
  4. package/lib/config/schema.mjs +1 -1
  5. package/lib/doctor/diagnosis.mjs +20 -0
  6. package/lib/doctor/index.mjs +5 -0
  7. package/lib/embed/worker.mjs +5 -0
  8. package/lib/env-config.mjs +10 -2
  9. package/lib/export-validate.mjs +34 -2
  10. package/lib/hooks/guard-bash.mjs +3 -1
  11. package/lib/host/readiness.mjs +109 -0
  12. package/lib/host-disposition.mjs +10 -1
  13. package/lib/install/stage-project.mjs +41 -4
  14. package/lib/intake/prepare.mjs +2 -0
  15. package/lib/mcp/destructive-approval.mjs +57 -0
  16. package/lib/mcp/server.mjs +209 -35
  17. package/lib/mcp/tool-rate-limit.mjs +47 -0
  18. package/lib/mcp/tool-safety.mjs +94 -0
  19. package/lib/mcp/tool-surface-parity.mjs +60 -0
  20. package/lib/mcp/tools/orchestration-run.mjs +45 -7
  21. package/lib/mcp/tools/project.mjs +25 -8
  22. package/lib/mcp/tools/storage.mjs +9 -1
  23. package/lib/mcp/tools/web-search-governance.mjs +96 -0
  24. package/lib/mcp/tools/web-search.mjs +6 -44
  25. package/lib/mcp-platform-config.mjs +27 -18
  26. package/lib/oracle/daemon-entry.mjs +6 -0
  27. package/lib/orchestration/runtime.mjs +81 -42
  28. package/lib/orchestration/web-capability.mjs +59 -0
  29. package/lib/orchestration/worker.mjs +263 -19
  30. package/lib/orchestration-policy.mjs +36 -0
  31. package/lib/output-quality.mjs +61 -2
  32. package/lib/path-policy.mjs +56 -0
  33. package/lib/providers/secret-audit-wiring.mjs +35 -18
  34. package/lib/providers/secret-resolver.mjs +28 -13
  35. package/lib/registry/catalog.mjs +6 -0
  36. package/lib/registry/loader.mjs +7 -1
  37. package/lib/registry/validate.mjs +3 -3
  38. package/lib/sandbox.mjs +1 -1
  39. package/lib/service-manager.mjs +59 -9
  40. package/lib/storage/admin.mjs +7 -2
  41. package/package.json +6 -1
  42. package/registry/agent-manifest.json +117 -0
  43. package/registry/capabilities.json +1880 -0
  44. package/schemas/brand-voice.schema.json +24 -0
  45. package/schemas/capability-registry.schema.json +72 -0
  46. package/schemas/certification-run.schema.json +130 -0
  47. package/schemas/demo-recording.schema.json +46 -0
  48. package/schemas/eval-dataset.schema.json +79 -0
  49. package/schemas/execution-capability-profile.schema.json +46 -0
  50. package/schemas/execution-policy.schema.json +114 -0
  51. package/schemas/improvement-proposal.schema.json +65 -0
  52. package/schemas/mcp-tool-output.schema.json +61 -0
  53. package/schemas/platform-capabilities.schema.json +83 -0
  54. package/schemas/project-config.schema.json +227 -0
  55. package/schemas/project-demo.schema.json +60 -0
  56. package/schemas/provider-behavior-matrix.schema.json +91 -0
  57. package/schemas/scope.schema.json +197 -0
  58. package/schemas/specialist-trace.schema.json +107 -0
  59. package/schemas/team.schema.json +99 -0
  60. package/schemas/unified-registry.schema.json +548 -0
  61. package/scripts/sync-specialists.mjs +135 -12
  62. package/specialists/org/specialists/cx-researcher.json +1 -0
  63. package/specialists/prompts/cx-researcher.md +9 -8
  64. package/vendor/pandoc-ext/README.md +3 -0
  65. package/vendor/pandoc-ext/diagram.lua +687 -0
@@ -18,6 +18,9 @@ import { loadConstructEnv, prepareConstructEnv } from '../runtime-env.mjs';
18
18
  import { getInstalledVersion } from '../version.mjs';
19
19
  import { getDeploymentMode } from '../deployment-mode.mjs';
20
20
  import { withGenAiSpan, GenAiAttrs, extractTraceContext, injectTraceContext } from '../telemetry/otel-tracer.mjs';
21
+ import { TOOL_SAFETY, DEFAULT_OUTPUT_SCHEMA } from './tool-safety.mjs';
22
+ import { ToolRateLimiter, ToolRateLimited } from './tool-rate-limit.mjs';
23
+ import { appendAuditRecord } from '../audit-trail.mjs';
21
24
 
22
25
  // Apply config.env values to process.env, letting config.env win over shell env
23
26
  // so telemetry/OpenRouter credentials are always correct regardless of host env.
@@ -63,6 +66,8 @@ import { modelResolve, triageRecommend, workflowInvoke, capabilityDescribe, exec
63
66
  import { authorArtifact } from './tools/artifact-author.mjs';
64
67
  import { findTool } from './tools/find-tool.mjs';
65
68
  import { recoverToolName, recordToolNameMiss, isGatewayName } from './tool-recovery.mjs';
69
+ import { assertCoreSubsetOfCatalog } from './tool-surface-parity.mjs';
70
+ import { enableSecretAuditTrail } from '../providers/secret-audit-wiring.mjs';
66
71
  import { buildOrchestrationReadiness } from '../orchestration/readiness.mjs';
67
72
 
68
73
  const DEFAULT_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -125,12 +130,25 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
125
130
 
126
131
  // The full construct-mcp tool catalog. Only a curated core is exposed flat in
127
132
  // ListTools; the long tail is reachable through the construct_call meta-tool, so
128
- // the serialized tool surface stays small on every host and model. The 71-tool
129
- // flat surface alone (~10.6k tokens) overran a 32k local-model window.
133
+ // the serialized tool surface stays small on every host and model. The 75-tool
134
+ // flat surface alone (~14.8k tokens) overran a 32k local-model window.
135
+
136
+ // Every tool def gains a declared outputSchema and a safety block (class, fs/
137
+ // network/process scope) from TOOL_SAFETY. A tool missing from that table throws
138
+ // here rather than shipping unclassified — see lib/mcp/tool-safety.mjs.
139
+
140
+ function withSafetyEnvelope(def) {
141
+ const safety = TOOL_SAFETY[def.name];
142
+ if (!safety) {
143
+ throw new Error(`tool-safety: "${def.name}" has no safety classification — add one to lib/mcp/tool-safety.mjs`);
144
+ }
145
+ return { ...def, outputSchema: def.outputSchema ?? DEFAULT_OUTPUT_SCHEMA, safety };
146
+ }
130
147
 
131
148
  const ALL_TOOL_DEFS = [
132
149
  {
133
150
  name: 'agent_health',
151
+ outputSchema: { type: 'object' },
134
152
  description: 'Returns agent health summaries from the most recent performance review.',
135
153
  inputSchema: {
136
154
  type: 'object',
@@ -144,6 +162,7 @@ const ALL_TOOL_DEFS = [
144
162
  },
145
163
  {
146
164
  name: 'summarize_diff',
165
+ outputSchema: { type: 'object' },
147
166
  description: 'Summarizes the git diff between the current state and a base ref.',
148
167
  inputSchema: {
149
168
  type: 'object',
@@ -161,6 +180,7 @@ const ALL_TOOL_DEFS = [
161
180
  },
162
181
  {
163
182
  name: 'scan_file',
183
+ outputSchema: { type: 'object' },
164
184
  description: 'Scans a file for secrets and code quality issues.',
165
185
  inputSchema: {
166
186
  type: 'object',
@@ -175,6 +195,7 @@ const ALL_TOOL_DEFS = [
175
195
  },
176
196
  {
177
197
  name: 'extract_document_text',
198
+ outputSchema: { type: 'object' },
178
199
  description: 'Extracts readable text from a local document path. Uses node-native extractors (unpdf/mammoth) first; escalates to the docling Python sidecar or whisper.cpp when needed (same pipeline as `construct ingest`). Supports PDF, DOCX, XLSX, PPTX, HTML, plain text, email, and transcripts.',
179
200
  inputSchema: {
180
201
  type: 'object',
@@ -193,6 +214,7 @@ const ALL_TOOL_DEFS = [
193
214
  },
194
215
  {
195
216
  name: 'ingest_document',
217
+ outputSchema: { type: 'object' },
196
218
  description: 'Converts a local document into a normalized markdown file, placing it into an indexed project path by default.',
197
219
  inputSchema: {
198
220
  type: 'object',
@@ -227,6 +249,7 @@ const ALL_TOOL_DEFS = [
227
249
  },
228
250
  {
229
251
  name: 'infer_document_schema',
252
+ outputSchema: { type: 'object' },
230
253
  description: 'Infers a structured field schema from a local document using AI. Returns field names, types, formats, examples, and confidence. Supports all document types handled by extract_document_text. Pass multiple file_paths to get a reconciled unified schema across documents.',
231
254
  inputSchema: {
232
255
  type: 'object',
@@ -265,6 +288,7 @@ const ALL_TOOL_DEFS = [
265
288
  },
266
289
  {
267
290
  name: 'list_schema_artifacts',
291
+ outputSchema: { type: 'object' },
268
292
  description: 'Lists all inferred schema artifacts (.schema.json files) in the project.',
269
293
  inputSchema: {
270
294
  type: 'object',
@@ -278,6 +302,7 @@ const ALL_TOOL_DEFS = [
278
302
  },
279
303
  {
280
304
  name: 'storage_status',
305
+ outputSchema: { type: 'object' },
281
306
  description: 'Returns SQL, local vector index, and ingested-artifact status for the current project.',
282
307
  inputSchema: {
283
308
  type: 'object',
@@ -367,6 +392,7 @@ const ALL_TOOL_DEFS = [
367
392
  },
368
393
  {
369
394
  name: 'project_context',
395
+ outputSchema: { type: 'object' },
370
396
  description: 'Returns project context: .cx/context.md content, recent commits, and working tree status.',
371
397
  inputSchema: {
372
398
  type: 'object',
@@ -380,6 +406,7 @@ const ALL_TOOL_DEFS = [
380
406
  },
381
407
  {
382
408
  name: 'get_skill',
409
+ outputSchema: { type: 'object' },
383
410
  description: 'Reads a specific skill playbook from the Construct knowledge base (e.g. "docs/adr-workflow", "roles/engineer", "architecture/security-arch").',
384
411
  inputSchema: {
385
412
  type: 'object',
@@ -391,6 +418,7 @@ const ALL_TOOL_DEFS = [
391
418
  },
392
419
  {
393
420
  name: 'orchestration_policy',
421
+ outputSchema: { type: 'object' },
394
422
  description: 'Classifies a request into intent, execution track, specialists, approval boundaries, and the contract chain that applies. The contractChain field names the typed producer→consumer handoffs expected for this dispatch plan. When `candidates` is supplied, also returns `contextPackets` keyed by specialist with the role-filtered artifact bundle each specialist should receive (omitted artifacts include the reason they were dropped).',
395
423
  inputSchema: {
396
424
  type: 'object',
@@ -442,6 +470,7 @@ const ALL_TOOL_DEFS = [
442
470
  },
443
471
  {
444
472
  name: 'list_skills',
473
+ outputSchema: { type: 'object' },
445
474
  description: 'Lists all available categories and playbooks in the Construct knowledge base.',
446
475
  inputSchema: {
447
476
  type: 'object',
@@ -450,6 +479,7 @@ const ALL_TOOL_DEFS = [
450
479
  },
451
480
  {
452
481
  name: 'worker_run',
482
+ outputSchema: { type: 'object' },
453
483
  description: 'Runs a bounded shell command via the worker plane: timeout, path-policy denial, restricted env, stdout/stderr artifacts under `.cx/runtime/worker/<jobId>.{stdout,stderr}.log`. When `graphId` + `nodeId` are supplied, an evidence record is appended to that task graph node so it can transition to `done`. Emits `worker.started` / `worker.completed` / `evidence.recorded` trace events correlated by `traceId`.',
454
484
  inputSchema: {
455
485
  type: 'object',
@@ -475,6 +505,7 @@ const ALL_TOOL_DEFS = [
475
505
  },
476
506
  {
477
507
  name: 'broker_check',
508
+ outputSchema: { type: 'object' },
478
509
  description: 'Queries the MCP broker policy gate for a pending action without executing it. Agents call this BEFORE attempting a high-risk action so the response (allowed / approvalRequired / reason / source) can be surfaced to the user in the agent\'s voice rather than triggering a denial after the fact. In solo mode the broker is off by default — returns `brokerActive: false` with `allowed: true` so the call is cheap and agents don\'t waste tokens on an inactive gate. Always emits a `tool.called` trace event for audit-trail parity. Reads specialists/org fence rules in team / enterprise mode.',
479
510
  inputSchema: {
480
511
  type: 'object',
@@ -491,6 +522,7 @@ const ALL_TOOL_DEFS = [
491
522
  },
492
523
  {
493
524
  name: 'agent_contract',
525
+ outputSchema: { type: 'object' },
494
526
  description: 'Looks up explicit agent-to-agent service contracts (from specialists/org). Specialists should call this at the start of a handoff to see the expected input shape, preconditions, and what postconditions they must satisfy. Use without args to get all contracts; pass producer/consumer to narrow; pass id for a specific contract.',
495
527
  inputSchema: {
496
528
  type: 'object',
@@ -503,6 +535,7 @@ const ALL_TOOL_DEFS = [
503
535
  },
504
536
  {
505
537
  name: 'find_tool',
538
+ outputSchema: { type: 'object' },
506
539
  description: 'Find Construct tools by intent when you do not know the exact name. Describe what you want to do; returns the best-matching tools with their input schemas, ranked by hybrid semantic + lexical relevance. Then invoke a result via the `call` gateway (or directly if it is a flat tool).',
507
540
  inputSchema: {
508
541
  type: 'object',
@@ -515,6 +548,7 @@ const ALL_TOOL_DEFS = [
515
548
  },
516
549
  {
517
550
  name: 'get_template',
551
+ outputSchema: { type: 'object' },
518
552
  description: 'Reads a doc template by name (e.g. "prd", "meta-prd", "prfaq", "evidence-brief", "adr", "runbook"). Resolves .cx/templates/docs/{name}.md first, then templates/docs/{name}.md.',
519
553
  inputSchema: {
520
554
  type: 'object',
@@ -526,11 +560,14 @@ const ALL_TOOL_DEFS = [
526
560
  },
527
561
  {
528
562
  name: 'list_templates',
563
+ outputSchema: { type: 'object' },
529
564
  description: 'Lists shipped and project-override doc templates.',
530
565
  inputSchema: { type: 'object', properties: {} },
531
566
  },
532
567
  {
533
568
  name: 'search_skills',
569
+ outputSchema: { type: 'object' },
570
+ category: 'retrieval',
534
571
  description: 'Searches for a pattern within the Construct knowledge base skills.',
535
572
  inputSchema: {
536
573
  type: 'object',
@@ -542,11 +579,13 @@ const ALL_TOOL_DEFS = [
542
579
  },
543
580
  {
544
581
  name: 'list_teams',
582
+ outputSchema: { type: 'object' },
545
583
  description: 'Lists all available Construct team templates with members, focus, and promotion gates.',
546
584
  inputSchema: { type: 'object', properties: {} },
547
585
  },
548
586
  {
549
587
  name: 'suggest_skills',
588
+ outputSchema: { type: 'object' },
550
589
  description: 'Rank skills from the central catalog for a natural-language intent. Optional specialistId filters entitlement metadata.',
551
590
  inputSchema: {
552
591
  type: 'object',
@@ -560,6 +599,7 @@ const ALL_TOOL_DEFS = [
560
599
  },
561
600
  {
562
601
  name: 'cx_trace',
602
+ outputSchema: { type: 'object' },
563
603
  description: 'Records an agent trace for observability. Call at the start of every significant task with your agent name and the user goal.',
564
604
  inputSchema: {
565
605
  type: 'object',
@@ -577,6 +617,7 @@ const ALL_TOOL_DEFS = [
577
617
  },
578
618
  {
579
619
  name: 'cx_score',
620
+ outputSchema: { type: 'object' },
580
621
  description: 'Attaches a quality score to a trace. Call after producing a significant deliverable.',
581
622
  inputSchema: {
582
623
  type: 'object',
@@ -591,6 +632,7 @@ const ALL_TOOL_DEFS = [
591
632
  },
592
633
  {
593
634
  name: 'cx_trace_update',
635
+ outputSchema: { type: 'object' },
594
636
  description: 'Updates an existing trace with output and metadata. Use when a trace was created early but the result becomes available later.',
595
637
  inputSchema: {
596
638
  type: 'object',
@@ -604,6 +646,7 @@ const ALL_TOOL_DEFS = [
604
646
  },
605
647
  {
606
648
  name: 'session_list',
649
+ outputSchema: { type: 'object' },
607
650
  description: 'List construct sessions for the current project. Returns distilled session index entries with id, project, status, and summary.',
608
651
  inputSchema: {
609
652
  type: 'object',
@@ -616,6 +659,7 @@ const ALL_TOOL_DEFS = [
616
659
  },
617
660
  {
618
661
  name: 'session_load',
662
+ outputSchema: { type: 'object' },
619
663
  description: 'Load a full distilled session record by ID. Returns summary, decisions, files changed, open questions, and task snapshot.',
620
664
  inputSchema: {
621
665
  type: 'object',
@@ -628,6 +672,7 @@ const ALL_TOOL_DEFS = [
628
672
  },
629
673
  {
630
674
  name: 'session_search',
675
+ outputSchema: { type: 'object' },
631
676
  description: 'Search sessions by keyword in summary or project name.',
632
677
  inputSchema: {
633
678
  type: 'object',
@@ -640,6 +685,7 @@ const ALL_TOOL_DEFS = [
640
685
  },
641
686
  {
642
687
  name: 'session_save',
688
+ outputSchema: { type: 'object' },
643
689
  description: 'Update the active session with distilled context: summary, decisions, files changed, open questions, task snapshot.',
644
690
  inputSchema: {
645
691
  type: 'object',
@@ -658,6 +704,8 @@ const ALL_TOOL_DEFS = [
658
704
  },
659
705
  {
660
706
  name: 'memory_search',
707
+ outputSchema: { type: 'object' },
708
+ category: 'retrieval',
661
709
  description: 'Search the observation store for patterns, decisions, and insights learned by specialists across sessions. Returns semantically matched observations scoped by role, category, or project.',
662
710
  inputSchema: {
663
711
  type: 'object',
@@ -673,6 +721,7 @@ const ALL_TOOL_DEFS = [
673
721
  },
674
722
  {
675
723
  name: 'memory_add_observations',
724
+ outputSchema: { type: 'object' },
676
725
  description: 'Record observations (patterns, insights, decisions, anti-patterns) that specialists discover during work. These are indexed for semantic search and surface in future sessions.',
677
726
  inputSchema: {
678
727
  type: 'object',
@@ -699,6 +748,7 @@ const ALL_TOOL_DEFS = [
699
748
  },
700
749
  {
701
750
  name: 'memory_create_entities',
751
+ outputSchema: { type: 'object' },
702
752
  description: 'Track recurring entities (components, services, APIs, dependencies) that specialists encounter. Enables "what do we know about X?" queries.',
703
753
  inputSchema: {
704
754
  type: 'object',
@@ -723,6 +773,7 @@ const ALL_TOOL_DEFS = [
723
773
  },
724
774
  {
725
775
  name: 'memory_recent',
776
+ outputSchema: { type: 'object' },
726
777
  description: 'Returns the most recent observations for the current project, deduplicated by (role, summary). Use this when the session-start hint indicates prior observations are available — fetch them on demand instead of paying for them every session.',
727
778
  inputSchema: {
728
779
  type: 'object',
@@ -735,6 +786,8 @@ const ALL_TOOL_DEFS = [
735
786
  },
736
787
  {
737
788
  name: 'rovo_search',
789
+ outputSchema: { type: 'object' },
790
+ category: 'retrieval',
738
791
  description: 'Cross-system semantic search via Atlassian Rovo. Searches Jira, Confluence, and other accessible sources. Returns excerpts with source attribution. Does NOT store results in the observation store. Use for broad queries across all sources, not just focal projects.',
739
792
  inputSchema: {
740
793
  type: 'object',
@@ -748,6 +801,7 @@ const ALL_TOOL_DEFS = [
748
801
  },
749
802
  {
750
803
  name: 'efficiency_snapshot',
804
+ outputSchema: { type: 'object' },
751
805
  description: 'Returns the read-efficiency snapshot for the current session — repeated reads, large reads, hot-spot files, and recommendations. Use this when investigating why a session feels slow or to surface optimization opportunities.',
752
806
  inputSchema: {
753
807
  type: 'object',
@@ -758,6 +812,7 @@ const ALL_TOOL_DEFS = [
758
812
  },
759
813
  {
760
814
  name: 'session_usage',
815
+ outputSchema: { type: 'object' },
761
816
  description: 'Returns locally recorded interaction token and cost usage for the current Construct session.',
762
817
  inputSchema: {
763
818
  type: 'object',
@@ -769,6 +824,8 @@ const ALL_TOOL_DEFS = [
769
824
  },
770
825
  {
771
826
  name: 'provider_fetch',
827
+ outputSchema: { type: 'object' },
828
+ category: 'retrieval',
772
829
  description: 'Look up current data for a configured repo, project, or team. This is an internal lookup against sources the operator has already authorized (set in config.env). Call this immediately — no user approval needed — whenever the user asks about a specific repo, project, or team name (e.g. "what is project iverson", "cloud-reliability status", "PLAT issues"). Pass the user\'s query and the tool resolves the right source automatically. Returns repo metadata, README, docs, open PRs, issues, and recent commits, then stores them as observations.',
773
830
  inputSchema: {
774
831
  type: 'object',
@@ -783,6 +840,7 @@ const ALL_TOOL_DEFS = [
783
840
  },
784
841
  {
785
842
  name: 'scope_show',
843
+ outputSchema: { type: 'object' },
786
844
  description: 'Return the active Construct scope (id, displayName, roles, departments, intake taxonomy, doc templates). Use when a specialist needs to know which role set, classification taxonomy, or doc templates apply before drafting work.',
787
845
  inputSchema: {
788
846
  type: 'object',
@@ -794,11 +852,13 @@ const ALL_TOOL_DEFS = [
794
852
  },
795
853
  {
796
854
  name: 'scope_list',
855
+ outputSchema: { type: 'object' },
797
856
  description: 'List the curated org scope catalog (rnd, operations, creative, research) with role/department counts. Use to discover which scopes are available before suggesting `construct scope set`.',
798
857
  inputSchema: { type: 'object', properties: {} },
799
858
  },
800
859
  {
801
860
  name: 'scope_drafts',
861
+ outputSchema: { type: 'object' },
802
862
  description: 'List in-progress draft scopes under `.cx/scopes/draft-*` and any user-defined custom scope at `.cx/scope.json`. Use to see what scope work is pending before scaffolding another draft.',
803
863
  inputSchema: {
804
864
  type: 'object',
@@ -807,6 +867,7 @@ const ALL_TOOL_DEFS = [
807
867
  },
808
868
  {
809
869
  name: 'scope_health',
870
+ outputSchema: { type: 'object' },
810
871
  description: 'Per-scope health rollup over a window: observation count, per-role outcome runs and success rates. Use to check whether a scope is producing data before recommending changes or archive.',
811
872
  inputSchema: {
812
873
  type: 'object',
@@ -819,6 +880,7 @@ const ALL_TOOL_DEFS = [
819
880
  },
820
881
  {
821
882
  name: 'outcomes_summary',
883
+ outputSchema: { type: 'object' },
822
884
  description: 'Read `.cx/outcomes/_summary.json` (per-role success rate, 30-day trend). Pass `aggregate=true` to rebuild the summary from JSONL outcome files first. Use to ground tiebreakers and improvement suggestions in real specialist performance.',
823
885
  inputSchema: {
824
886
  type: 'object',
@@ -830,6 +892,7 @@ const ALL_TOOL_DEFS = [
830
892
  },
831
893
  {
832
894
  name: 'outcomes_record',
895
+ outputSchema: { type: 'object' },
833
896
  description: 'Append a specialist outcome line to `.cx/outcomes/<role>.jsonl` (writes durable state — requires `confirm=true`). Use when a specialist wants to self-report success/failure outside the automatic agent-tracker path.',
834
897
  inputSchema: {
835
898
  type: 'object',
@@ -850,6 +913,7 @@ const ALL_TOOL_DEFS = [
850
913
  },
851
914
  {
852
915
  name: 'knowledge_add',
916
+ outputSchema: { type: 'object' },
853
917
  description: 'Persist a research finding as `.cx/knowledge/external/research/<slug>.md` with research-specific frontmatter (topic, confidence, sources, expiresAt, scope). Writes durable state — requires `confirm=true`. `confidence=confirmed` requires at least one source.',
854
918
  inputSchema: {
855
919
  type: 'object',
@@ -880,6 +944,7 @@ const ALL_TOOL_DEFS = [
880
944
  },
881
945
  {
882
946
  name: 'scope_create',
947
+ outputSchema: { type: 'object' },
883
948
  description: 'Scaffold a draft org scope under `.cx/scopes/draft-<id>/` (requirements.md + scope.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/guides/concepts/scope-lifecycle.md` after creation.',
884
949
  inputSchema: {
885
950
  type: 'object',
@@ -911,6 +976,7 @@ const ALL_TOOL_DEFS = [
911
976
  },
912
977
  {
913
978
  name: 'scope_archive',
979
+ outputSchema: { type: 'object' },
914
980
  description: 'Archive a curated scope: moves `specialists/org/scopes/<id>.json` and its intake table into `archive/scopes/<id>/` with an archive note. Destructive — requires `confirm=true` and a substantive `reason` (>=8 chars). Observations and outcomes are preserved.',
915
981
  inputSchema: {
916
982
  type: 'object',
@@ -924,11 +990,14 @@ const ALL_TOOL_DEFS = [
924
990
  },
925
991
  {
926
992
  name: 'sandbox_list',
993
+ outputSchema: { type: 'object' },
927
994
  description: 'List Construct sandboxes under `~/.cx/sandboxes/` (id, path, createdAt). Use to find an isolated environment for QA or dry-runs without polluting the active project.',
928
995
  inputSchema: { type: 'object', properties: {} },
929
996
  },
930
997
  {
931
998
  name: 'knowledge_graph_ask',
999
+ outputSchema: { type: 'object' },
1000
+ category: 'retrieval',
932
1001
  description: 'GraphRAG-style global query over the entity graph in `.cx/observations/`. Detects communities via label propagation, ranks them by BM25 against the query, and returns each top community with its central members and extractive summary. Use for "tell me about how X relates across the project" questions that pure semantic retrieval handles poorly.',
933
1002
  inputSchema: {
934
1003
  type: 'object',
@@ -945,6 +1014,7 @@ const ALL_TOOL_DEFS = [
945
1014
  },
946
1015
  {
947
1016
  name: 'learning_status',
1017
+ outputSchema: { type: 'object' },
948
1018
  description: 'One-shot mirror of `npm run learning:status`: active scope, observation counts (last 24h + total), research finding count, per-role outcome rollup. Use to answer "is Construct learning?" without spawning a shell.',
949
1019
  inputSchema: {
950
1020
  type: 'object',
@@ -953,6 +1023,7 @@ const ALL_TOOL_DEFS = [
953
1023
  },
954
1024
  {
955
1025
  name: 'document_export',
1026
+ outputSchema: { type: 'object' },
956
1027
  description: 'Convert a markdown file into a distributable document — PDF, DOCX, legacy DOC (LibreOffice), deck HTML, PPTX, HTML, RTF, ODT, EPUB, LaTeX, plain text, or Markdown — via Pandoc and Typst (PDF engine) plus optional pptxgenjs/LibreOffice, per ADR-0024. Engines are optional dependencies discovered at runtime — the tool returns a structured "install <binary>" error when tooling is absent, never crashes. Use `detect_only=true` to check availability without running an export.',
957
1028
  inputSchema: {
958
1029
  type: 'object',
@@ -968,6 +1039,7 @@ const ALL_TOOL_DEFS = [
968
1039
  },
969
1040
  {
970
1041
  name: 'publish_detect',
1042
+ outputSchema: { type: 'object' },
971
1043
  description: 'Detect availability of publish pipeline tooling: Pandoc/Typst export, pandoc-ext/diagram figures, and VHS terminal demos.',
972
1044
  inputSchema: {
973
1045
  type: 'object',
@@ -980,6 +1052,7 @@ const ALL_TOOL_DEFS = [
980
1052
  },
981
1053
  {
982
1054
  name: 'publish_run',
1055
+ outputSchema: { type: 'object' },
983
1056
  description: 'Run the publish pipeline: export markdown with optional figure filter and optional demo recordings. Use dry_run=true to probe tooling only.',
984
1057
  inputSchema: {
985
1058
  type: 'object',
@@ -1000,6 +1073,8 @@ const ALL_TOOL_DEFS = [
1000
1073
  },
1001
1074
  {
1002
1075
  name: 'knowledge_search',
1076
+ outputSchema: { type: 'object' },
1077
+ category: 'retrieval',
1003
1078
  description: 'Search Construct\'s own documentation, knowledge base, and distilled embed observations. Call this immediately — no approval needed — when the user asks what Construct is, how a feature works, what commands exist, or anything about Construct\'s architecture or configuration. Also searches embed observations from GitHub, Jira, and other configured sources. Returns relevant excerpts with source file and heading.',
1004
1079
  inputSchema: {
1005
1080
  type: 'object',
@@ -1014,6 +1089,7 @@ const ALL_TOOL_DEFS = [
1014
1089
  },
1015
1090
  {
1016
1091
  name: 'workflow_init',
1092
+ outputSchema: { type: 'object' },
1017
1093
  description: 'Initialize a new workflow for the current project. Creates plan.md state if not already present and returns the initial workflow envelope.',
1018
1094
  inputSchema: {
1019
1095
  type: 'object',
@@ -1026,6 +1102,7 @@ const ALL_TOOL_DEFS = [
1026
1102
  },
1027
1103
  {
1028
1104
  name: 'workflow_add_task',
1105
+ outputSchema: { type: 'object' },
1029
1106
  description: 'Add a task to the current workflow. Pass `request` for intent-based routing (the classifier picks track + specialist) or pass explicit task fields (`key`, `title`, etc.) for manual entry.',
1030
1107
  inputSchema: {
1031
1108
  type: 'object',
@@ -1052,6 +1129,7 @@ const ALL_TOOL_DEFS = [
1052
1129
  },
1053
1130
  {
1054
1131
  name: 'workflow_update_task',
1132
+ outputSchema: { type: 'object' },
1055
1133
  description: 'Update fields on an existing workflow task. Requires the task `key`. Only fields supplied are changed.',
1056
1134
  inputSchema: {
1057
1135
  type: 'object',
@@ -1072,6 +1150,7 @@ const ALL_TOOL_DEFS = [
1072
1150
  },
1073
1151
  {
1074
1152
  name: 'workflow_needs_main_input',
1153
+ outputSchema: { type: 'object' },
1075
1154
  description: 'Mark a workflow task as blocked pending user input. Sets status to blocked_needs_user and writes a packet describing the blocker for the orchestrator to surface.',
1076
1155
  inputSchema: {
1077
1156
  type: 'object',
@@ -1087,6 +1166,7 @@ const ALL_TOOL_DEFS = [
1087
1166
  },
1088
1167
  {
1089
1168
  name: 'workflow_validate',
1169
+ outputSchema: { type: 'object' },
1090
1170
  description: 'Validate the current workflow state against the schema and run consistency checks (no orphan tasks, no circular dependencies, every owner resolves to a known persona).',
1091
1171
  inputSchema: {
1092
1172
  type: 'object',
@@ -1097,6 +1177,7 @@ const ALL_TOOL_DEFS = [
1097
1177
  },
1098
1178
  {
1099
1179
  name: 'workflow_status',
1180
+ outputSchema: { type: 'object' },
1100
1181
  description: 'Return the full workflow snapshot for the current project: tasks, summary, alignment health, and the public-health surface used by the dashboard.',
1101
1182
  inputSchema: {
1102
1183
  type: 'object',
@@ -1107,6 +1188,7 @@ const ALL_TOOL_DEFS = [
1107
1188
  },
1108
1189
  {
1109
1190
  name: 'workflow_contract_validate',
1191
+ outputSchema: { type: 'object' },
1110
1192
  description: 'Validate a producer→consumer handoff against specialists/org. Required when a specialist hands off to another role: enforces input.mustContain, output schema, disk-artifact postconditions, and binary postconditions per producer (rubber-stamp prevention, post-hoc threat-model prevention, etc.). Self-enforcing: a producer with binary rules MUST pass `packet`, or the call itself is a contract violation.',
1111
1193
  inputSchema: {
1112
1194
  type: 'object',
@@ -1123,6 +1205,7 @@ const ALL_TOOL_DEFS = [
1123
1205
  },
1124
1206
  {
1125
1207
  name: 'workflow_import_plan',
1208
+ outputSchema: { type: 'object' },
1126
1209
  description: 'Bulk-add tasks from a markdown plan to the current workflow. Parses headings and bullet structure to extract task titles, owners, and acceptance criteria.',
1127
1210
  inputSchema: {
1128
1211
  type: 'object',
@@ -1142,6 +1225,7 @@ const ALL_TOOL_DEFS = [
1142
1225
  },
1143
1226
  {
1144
1227
  name: 'cx_trace_telemetry',
1228
+ outputSchema: { type: 'object' },
1145
1229
  description: 'Record a single CX telemetry trace for an agent invocation. Use to log start/end, model used, token cost, and outcome verdict for performance review.',
1146
1230
  inputSchema: {
1147
1231
  type: 'object',
@@ -1155,6 +1239,7 @@ const ALL_TOOL_DEFS = [
1155
1239
  },
1156
1240
  {
1157
1241
  name: 'artifact_workflow',
1242
+ outputSchema: { type: 'object' },
1158
1243
  description: 'Plan a manifest-backed document artifact workflow and return a truthful provenance report. It separates planned steps from locally executed validation/export and never claims a host-planned specialist review or rewrite was completed. Durable local export requires approval_mode=allow-durable-write.',
1159
1244
  inputSchema: {
1160
1245
  type: 'object',
@@ -1172,6 +1257,7 @@ const ALL_TOOL_DEFS = [
1172
1257
  },
1173
1258
  {
1174
1259
  name: 'author_artifact',
1260
+ outputSchema: { type: 'object' },
1175
1261
  description: 'Materialize a typed Construct artifact you have drafted (prd, prd-platform, prd-business, meta-prd, adr, rfc, research-brief, evidence-brief, runbook) to disk and run the release gate. YOU draft the full markdown — start with a single # title and include the type\'s required ## sections (call get_template first for the shape) — and pass it as draft_markdown; the canonical file is written and the gate verdict + errors are returned so you can fix and re-call. This is the Construct author→materialize→validate pass for supported hosts.',
1176
1262
  inputSchema: {
1177
1263
  type: 'object',
@@ -1185,6 +1271,7 @@ const ALL_TOOL_DEFS = [
1185
1271
  },
1186
1272
  {
1187
1273
  name: 'model_resolve',
1274
+ outputSchema: { type: 'object' },
1188
1275
  description: 'Resolve which model an embedded Construct workflow should use given the host/IDE provider context. Precedence: host model → same-provider-family fallback → Construct tier default → structured config error. Never reads or returns credential values (requiresCredential is a boolean) and never claims unverified provider health. Read-only; performs no writes.',
1189
1276
  inputSchema: {
1190
1277
  type: 'object',
@@ -1201,6 +1288,7 @@ const ALL_TOOL_DEFS = [
1201
1288
  },
1202
1289
  {
1203
1290
  name: 'triage_recommend',
1291
+ outputSchema: { type: 'object' },
1204
1292
  description: 'Classify an artifact and return a role-aware plan (primary owner, role chain with rationale, suggested skills, evidence requirements, expected outputs, approval requirements, risks, next steps, canExecute) WITHOUT enqueuing or executing. Classification confidence is reported distinctly from any generation confidence. Read-only; performs no durable write.',
1205
1293
  inputSchema: {
1206
1294
  type: 'object',
@@ -1218,6 +1306,7 @@ const ALL_TOOL_DEFS = [
1218
1306
  },
1219
1307
  {
1220
1308
  name: 'workflow_invoke',
1309
+ outputSchema: { type: 'object' },
1221
1310
  description: 'Invoke a named Construct workflow (roles/skills) non-interactively and return a provenanced execution plan: selected roles, rationale, applied skills, resolved model, evidence requirements, output contract, risks, and a traceId. Construct returns the orchestration plan; the host runtime performs specialist reasoning. Durable writes occur ONLY when approval_mode is allow-durable-write; proposal-only and requires-human-approval perform no durable writes.',
1222
1311
  inputSchema: {
1223
1312
  type: 'object',
@@ -1239,6 +1328,7 @@ const ALL_TOOL_DEFS = [
1239
1328
  },
1240
1329
  {
1241
1330
  name: 'capability_describe',
1331
+ outputSchema: { type: 'object' },
1242
1332
  description: 'Describe what this Construct install can do: versions, contract interfaces (CLI/MCP/SDK), roles, skills, workflows, schemas, models/providers, policies, telemetry posture, and plugins. Read-only and secret-free — provider entries carry env-key names and a configured boolean only, never credential values. Reads live registries so the published contract cannot drift from reality.',
1243
1333
  inputSchema: {
1244
1334
  type: 'object',
@@ -1249,6 +1339,7 @@ const ALL_TOOL_DEFS = [
1249
1339
  },
1250
1340
  {
1251
1341
  name: 'construct_execution_resolve',
1342
+ outputSchema: { type: 'object' },
1252
1343
  description: 'Resolve the execution-capability contract for an embedded workflow BEFORE/at workflow start: returns executionMode (construct-orchestrated | construct-prompt-only | host-direct | same-family-fallback), constructCapabilitiesActive (subset of personas/skills/workflow-routing/prompt-envelope), degraded + machine-readable degradationReason, requestedStrategy vs effectiveStrategy, and the resolved provider/model. Descriptive, not enforced: reports what Construct planned and can resolve a model for, never an observation that the host ran personas (see the semantics field). Read-only and secret-free.',
1253
1344
  inputSchema: {
1254
1345
  type: 'object',
@@ -1267,6 +1358,7 @@ const ALL_TOOL_DEFS = [
1267
1358
  },
1268
1359
  {
1269
1360
  name: 'orchestration_run',
1361
+ outputSchema: { type: 'object' },
1270
1362
  description: 'EXECUTE a real multi-specialist orchestration run and return per-specialist output — the executing counterpart to workflow_invoke (which only plans). For MCP hosts with no subagent primitive (VS Code/Copilot, Cursor), this is how you actually run a specialist chain: the engine owns orchestration, this tool is the thin client (ADR-0022). Solo runs execute in-process — no daemon, no port, no token; a remote/team orchestration service is opt-in via CONSTRUCT_ORCHESTRATION_URL. Real specialist output requires the `provider` worker backend (a provider key configured); the default `inline` backend prepares tasks only.',
1271
1363
  inputSchema: {
1272
1364
  type: 'object',
@@ -1288,6 +1380,8 @@ const ALL_TOOL_DEFS = [
1288
1380
  },
1289
1381
  {
1290
1382
  name: 'web_search',
1383
+ outputSchema: { type: 'object' },
1384
+ category: 'retrieval',
1291
1385
  description: 'Search the PUBLIC WEB and return CITED results — the only search surface that reaches the open web, kept distinct from knowledge_search / provider_fetch / repo search so it is never conflated or faked. Requires a governed provider (WEB_SEARCH_URL); without one it returns a typed degradation (capability-unavailable) and zero results, never source/repo results dressed as web. Every result carries a verifiable URL, a claim-relative class, and an Admiralty grade with derived confidence (ADR-0017; high reserved for A1/A2/B1).',
1292
1386
  inputSchema: {
1293
1387
  type: 'object',
@@ -1301,6 +1395,7 @@ const ALL_TOOL_DEFS = [
1301
1395
  },
1302
1396
  {
1303
1397
  name: 'orchestration_status',
1398
+ outputSchema: { type: 'object' },
1304
1399
  description: 'Inspect orchestration runs on the local Construct daemon: pass run_id for the full record (status, per-task status/executor/output/error), or omit it for a list of recent runs. Fails fast if the daemon is unreachable.',
1305
1400
  inputSchema: {
1306
1401
  type: 'object',
@@ -1312,6 +1407,7 @@ const ALL_TOOL_DEFS = [
1312
1407
  },
1313
1408
  {
1314
1409
  name: 'orchestration_readiness',
1410
+ outputSchema: { type: 'object' },
1315
1411
  description: 'Report whether this MCP session has the required Construct orchestration tools attached and reachable now. Returns a pass/fail verdict, typed reasonCode, one next step, required/observed/missing tools, and a redacted diagnostic bundle for support.',
1316
1412
  inputSchema: {
1317
1413
  type: 'object',
@@ -1326,7 +1422,7 @@ const ALL_TOOL_DEFS = [
1326
1422
  },
1327
1423
  },
1328
1424
  },
1329
- ];
1425
+ ].map(withSafetyEnvelope);
1330
1426
 
1331
1427
  // Curated flat core: high-frequency, low-arg tools the orchestrator and the
1332
1428
  // built-in Build/Plan agents actually reach for. Everything else collapses
@@ -1334,21 +1430,28 @@ const ALL_TOOL_DEFS = [
1334
1430
  // host/model; the long tail is reachable, just not front-loaded.
1335
1431
 
1336
1432
  const CORE_TOOL_NAMES = new Set([
1337
- 'orchestration_policy', 'get_skill', 'get_template', 'search_skills', 'knowledge_search',
1433
+ 'orchestration_policy', 'orchestration_run', 'get_skill', 'get_template', 'search_skills', 'knowledge_search',
1338
1434
  'memory_search', 'project_context', 'summarize_diff', 'find_tool',
1339
1435
  'author_artifact', 'document_export', 'publish_run', 'artifact_workflow',
1340
1436
  'workflow_invoke', 'triage_recommend', 'orchestration_readiness',
1341
1437
  ]);
1342
1438
 
1343
- const LONG_TAIL_DEFS = ALL_TOOL_DEFS.filter((t) => !CORE_TOOL_NAMES.has(t.name));
1344
1439
  const KNOWN_TOOL_NAMES = new Set(ALL_TOOL_DEFS.map((t) => t.name));
1345
1440
 
1441
+ // The core/long-tail split is hand-maintained beside the catalog; a typo in a
1442
+ // core name would silently drop a tool from both the flat surface and the gateway
1443
+ // enum. Fail fast at module load against the catalog as the single source of truth.
1444
+
1445
+ assertCoreSubsetOfCatalog(CORE_TOOL_NAMES, KNOWN_TOOL_NAMES);
1446
+
1447
+ const LONG_TAIL_DEFS = ALL_TOOL_DEFS.filter((t) => !CORE_TOOL_NAMES.has(t.name));
1448
+
1346
1449
  // One dispatcher for the long tail. `tool` is constrained to an enum of valid
1347
1450
  // names (≈1 token each — kills hallucinated names, the key small-model lever),
1348
1451
  // and the description carries a compact one-line catalog instead of ~10k of full
1349
1452
  // schemas. Dispatch reuses the same handlers via dispatchToolByName.
1350
1453
 
1351
- const CONSTRUCT_CALL_TOOL = {
1454
+ const CONSTRUCT_CALL_TOOL = withSafetyEnvelope({
1352
1455
  name: 'call',
1353
1456
  description:
1354
1457
  'Invoke any non-core Construct tool by name: provide `tool` and `args`. '
@@ -1363,7 +1466,7 @@ const CONSTRUCT_CALL_TOOL = {
1363
1466
  },
1364
1467
  required: ['tool'],
1365
1468
  },
1366
- };
1469
+ });
1367
1470
 
1368
1471
  export function exposedTools() {
1369
1472
  return [...ALL_TOOL_DEFS.filter((t) => CORE_TOOL_NAMES.has(t.name)), CONSTRUCT_CALL_TOOL];
@@ -1543,14 +1646,33 @@ async function resolveUnknownToolName(name, args = {}) {
1543
1646
  return { error: `Unknown tool: ${name}. Core tools are flat; invoke any other tool via { name: 'call', arguments: { tool: '<name>', args: {…} } }.` };
1544
1647
  }
1545
1648
 
1649
+ // Solo mode never instantiates lib/mcp/broker.mjs's Broker (role/policy-based,
1650
+ // wired only for team/enterprise), so the live dispatch path otherwise has no
1651
+ // rate bound and no audit trail in the default deployment. windowMs 0 disables,
1652
+ // mirroring the CONSTRUCT_MCP_TOOL_TIMEOUT_MS override convention below.
1653
+
1654
+ const toolRateLimiter = new ToolRateLimiter({
1655
+ windowMs: (() => {
1656
+ const raw = Number(process.env.CONSTRUCT_MCP_TOOL_RATE_WINDOW_MS);
1657
+ return Number.isFinite(raw) && raw >= 0 ? raw : undefined;
1658
+ })(),
1659
+ });
1660
+
1546
1661
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1547
1662
  const { name, arguments: args = {} } = request.params;
1663
+ const callStart = Date.now();
1548
1664
 
1549
1665
  // Extract W3C traceparent from params._meta (SEP-414 propagation). Tracing
1550
1666
  // must never break dispatch — a malformed _meta should not fail the call.
1551
1667
  let parentCtx = {};
1552
1668
  try { parentCtx = await extractTraceContext(request.params?._meta || {}); } catch { /* tracing optional */ }
1553
- void parentCtx;
1669
+
1670
+ // The `call` gateway's own class would conflate every long-tail tool's budget
1671
+ // into one bucket; rate-limit and audit-log the real underlying tool instead.
1672
+
1673
+ const innerTool = name === 'call' && typeof args?.tool === 'string' && !isGatewayName(args.tool) ? args.tool : null;
1674
+ const auditedTool = innerTool ?? name;
1675
+ const safetyClass = TOOL_SAFETY[auditedTool]?.class ?? 'read';
1554
1676
 
1555
1677
  // Bound every tool call. A tool that stalls (a stuck external extractor, a slow
1556
1678
  // model load, a wedged subprocess) must surface a clean timeout error to the
@@ -1561,38 +1683,84 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1561
1683
  return Number.isFinite(raw) && raw >= 0 ? raw : 120_000;
1562
1684
  })();
1563
1685
 
1564
- const dispatch = (async () => {
1565
- let result;
1566
- try {
1567
- result = await dispatchToolByName(name, args);
1568
- } catch (err) {
1569
- result = { error: err.message ?? String(err) };
1570
- }
1571
- return result;
1572
- })();
1573
-
1574
1686
  let toolResult;
1575
- if (!TOOL_TIMEOUT_MS) {
1576
- toolResult = await dispatch;
1577
- } else {
1578
- let timer;
1579
- const timeout = new Promise((_, reject) => {
1580
- timer = setTimeout(
1581
- () => reject(new Error(`tool ${name} timed out after ${Math.round(TOOL_TIMEOUT_MS / 1000)}s`)),
1582
- TOOL_TIMEOUT_MS,
1583
- );
1584
- });
1585
- try {
1586
- toolResult = await Promise.race([dispatch, timeout]);
1587
- } catch (err) {
1588
- toolResult = { error: err.message ?? String(err) };
1589
- } finally {
1590
- clearTimeout(timer);
1591
- }
1687
+ try {
1688
+ toolRateLimiter.check(auditedTool, safetyClass);
1689
+
1690
+ // A GenAI span per dispatch records the real underlying tool, its safety
1691
+ // class, and the serialized result size (a token proxy) for every call, so
1692
+ // per-tool calls/latency/errors are measured. Tracing never fails the call:
1693
+ // on any dispatch error the inner catch resolves to an { error } object, so
1694
+ // the span closes OK and the client still gets a structured error payload.
1695
+
1696
+ toolResult = await withGenAiSpan(
1697
+ `execute_tool ${auditedTool}`,
1698
+ { [GenAiAttrs.TOOL_NAME]: auditedTool, 'construct.tool.safety_class': safetyClass, [GenAiAttrs.MCP_METHOD]: 'tools/call' },
1699
+ async (span) => {
1700
+ const dispatch = (async () => {
1701
+ let result;
1702
+ try {
1703
+ result = await dispatchToolByName(name, args);
1704
+ } catch (err) {
1705
+ result = { error: err.message ?? String(err) };
1706
+ }
1707
+ return result;
1708
+ })();
1709
+
1710
+ let out;
1711
+ if (!TOOL_TIMEOUT_MS) {
1712
+ out = await dispatch;
1713
+ } else {
1714
+ let timer;
1715
+ const timeout = new Promise((_, reject) => {
1716
+ timer = setTimeout(
1717
+ () => reject(new Error(`tool ${name} timed out after ${Math.round(TOOL_TIMEOUT_MS / 1000)}s`)),
1718
+ TOOL_TIMEOUT_MS,
1719
+ );
1720
+ });
1721
+ try {
1722
+ out = await Promise.race([dispatch, timeout]);
1723
+ } catch (err) {
1724
+ out = { error: err.message ?? String(err) };
1725
+ } finally {
1726
+ clearTimeout(timer);
1727
+ }
1728
+ }
1729
+
1730
+ const isError = Boolean(out && typeof out === 'object' && 'error' in out);
1731
+ span.setAttribute('construct.tool.result_bytes', JSON.stringify(out ?? null).length);
1732
+ span.setAttribute('construct.tool.ok', !isError);
1733
+ return out;
1734
+ },
1735
+ parentCtx,
1736
+ );
1737
+ } catch (err) {
1738
+ if (err instanceof ToolRateLimited) toolResult = { error: err.message };
1739
+ else throw err;
1592
1740
  }
1593
1741
 
1742
+ // Value-free: tool name and safety class only, never call args or result
1743
+ // content. Logging must never break dispatch, matching the tracing guard above.
1744
+
1745
+ try {
1746
+ appendAuditRecord({
1747
+ ts: new Date().toISOString(),
1748
+ agent: 'mcp-server',
1749
+ tool: auditedTool,
1750
+ target: safetyClass,
1751
+ ok: !(toolResult && typeof toolResult === 'object' && 'error' in toolResult),
1752
+ duration_ms: Date.now() - callStart,
1753
+ });
1754
+ } catch { /* audit trail unavailable must not fail the call */ }
1755
+
1756
+ // Every tool now declares an outputSchema (see withSafetyEnvelope); the MCP SDK
1757
+ // client validates that declaration against the response and rejects a tool
1758
+ // call whose result omits structuredContent. toolResult is always a JSON object
1759
+ // across every dispatch branch, so it satisfies each tool's schema directly.
1760
+
1594
1761
  return {
1595
1762
  content: [{ type: 'text', text: JSON.stringify(toolResult, null, 2) }],
1763
+ structuredContent: toolResult,
1596
1764
  };
1597
1765
  });
1598
1766
 
@@ -1619,6 +1787,12 @@ const argv1Real = (() => { try { return realpathSync(process.argv[1]); } catch {
1619
1787
  if (fileURLToPath(import.meta.url) === argv1Real) {
1620
1788
  console.error('[construct-mcp] server started');
1621
1789
 
1790
+ // Orchestration and provider-backed tools resolve credentials in the server's
1791
+ // own process; wiring the sink at the entry records those op reads on the same
1792
+ // trail as the CLI. Scoped to the server entry so importing this module for a
1793
+ // test never installs the process-global sink.
1794
+ enableSecretAuditTrail();
1795
+
1622
1796
  // A long-running stdio server must survive a single malformed request: a
1623
1797
  // background rejection from one ingest (e.g. a broken docling sidecar that
1624
1798
  // settles late) must not terminate the process and close the client