hoi4-agent-tools 3.10.0 → 4.0.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +115 -147
  3. package/dist/bin/http.js +10 -0
  4. package/dist/bin/setup.js +3 -2
  5. package/dist/bin/stdio.js +10 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +40 -5
  7. package/dist/hoi4_agent_tools/core/canonical.js +18 -4
  8. package/dist/hoi4_agent_tools/core/configuration.js +7 -0
  9. package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
  10. package/dist/hoi4_agent_tools/core/domain-tools.js +65 -0
  11. package/dist/hoi4_agent_tools/core/domain-validation.js +5 -2
  12. package/dist/hoi4_agent_tools/core/engine.js +29 -2
  13. package/dist/hoi4_agent_tools/core/index.js +2 -2
  14. package/dist/hoi4_agent_tools/core/job-executor.js +14 -1
  15. package/dist/hoi4_agent_tools/core/job-operation-registration.js +15 -7
  16. package/dist/hoi4_agent_tools/core/job-store.js +78 -5
  17. package/dist/hoi4_agent_tools/core/job-transactions.js +31 -2
  18. package/dist/hoi4_agent_tools/core/job-worker-host.js +215 -116
  19. package/dist/hoi4_agent_tools/core/job-worker.js +90 -36
  20. package/dist/hoi4_agent_tools/core/operation-result.js +123 -10
  21. package/dist/hoi4_agent_tools/core/operation-tasks.js +49 -89
  22. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +25 -21
  23. package/dist/hoi4_agent_tools/core/server-registry.js +22 -5
  24. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
  25. package/dist/hoi4_agent_tools/core/source/cache.js +20 -10
  26. package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
  27. package/dist/hoi4_agent_tools/core/transactions.js +114 -23
  28. package/dist/hoi4_agent_tools/core/workspace.js +84 -8
  29. package/dist/hoi4_agent_tools/decision/ai.js +1 -1
  30. package/dist/hoi4_agent_tools/decision/service.js +28 -0
  31. package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
  32. package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
  33. package/dist/hoi4_agent_tools/event/operations.js +14 -1
  34. package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
  35. package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
  36. package/dist/hoi4_agent_tools/gui/validators.js +8 -6
  37. package/dist/hoi4_agent_tools/map/bmp.js +1 -1
  38. package/dist/hoi4_agent_tools/map/derived.js +748 -0
  39. package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
  40. package/dist/hoi4_agent_tools/map/model.js +103 -0
  41. package/dist/hoi4_agent_tools/map/operations.js +427 -58
  42. package/dist/hoi4_agent_tools/map/references.js +483 -0
  43. package/dist/hoi4_agent_tools/map/service.js +136 -61
  44. package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
  45. package/dist/hoi4_agent_tools/map/validation.js +3 -0
  46. package/dist/hoi4_agent_tools/map/world.js +1280 -0
  47. package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
  48. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
  49. package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
  50. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
  51. package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
  52. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +25 -5
  53. package/dist/hoi4_agent_tools/mcp/tools/event.js +18 -29
  54. package/dist/hoi4_agent_tools/mcp/tools/focus.js +4 -12
  55. package/dist/hoi4_agent_tools/mcp/tools/gui.js +3 -3
  56. package/dist/hoi4_agent_tools/mcp/tools/job.js +2 -2
  57. package/dist/hoi4_agent_tools/mcp/tools/map.js +13 -16
  58. package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
  59. package/dist/hoi4_agent_tools/mcp/tools/reference.js +22 -5
  60. package/dist/hoi4_agent_tools/mcp/tools/technology.js +16 -29
  61. package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
  62. package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +31 -8
  63. package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
  64. package/dist/hoi4_agent_tools/probability/operations.js +175 -13
  65. package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
  66. package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
  67. package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
  68. package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
  69. package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
  70. package/dist/hoi4_agent_tools/reference/operations.js +88 -19
  71. package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
  72. package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
  73. package/dist/hoi4_agent_tools/reference/service.js +6 -0
  74. package/dist/hoi4_agent_tools/schemas/event.js +78 -19
  75. package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
  76. package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
  77. package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
  78. package/dist/hoi4_agent_tools/schemas/map.js +122 -3
  79. package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
  80. package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
  81. package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
  82. package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
  83. package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
  84. package/dist/hoi4_agent_tools/schemas/technology.js +79 -21
  85. package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
  86. package/dist/hoi4_agent_tools/technology/operations.js +14 -1
  87. package/dist/hoi4_agent_tools/version.js +1 -1
  88. package/docs/README.md +7 -4
  89. package/docs/analysis.md +13 -0
  90. package/docs/configuration.md +281 -0
  91. package/docs/development.md +24 -2
  92. package/docs/events.md +27 -3
  93. package/docs/examples.md +6 -6
  94. package/docs/focus.md +125 -4
  95. package/docs/gui.md +98 -17
  96. package/docs/helper-expansion.md +8 -8
  97. package/docs/http.md +3 -1
  98. package/docs/jobs.md +19 -13
  99. package/docs/map.md +197 -6
  100. package/docs/mechanics.md +4 -0
  101. package/docs/probability.md +42 -15
  102. package/docs/reference.md +71 -4
  103. package/docs/setup.md +44 -11
  104. package/docs/technology.md +26 -1
  105. package/docs/third-party-notices.md +11 -0
  106. package/docs/tools.md +151 -42
  107. package/docs/troubleshooting.md +61 -0
  108. package/examples/clients/codex-config.toml +5 -2
  109. package/examples/clients/generic.json +2 -2
  110. package/examples/config/config.json +7 -0
  111. package/npm-shrinkwrap.json +151 -156
  112. package/package.json +13 -8
  113. package/schemas/configuration.schema.json +144 -128
  114. package/schemas/continuous-focus-palette.schema.json +1 -1
  115. package/schemas/custom-weighted-pool.schema.json +1 -1
  116. package/schemas/decision-inspect-input.schema.json +1 -1
  117. package/schemas/error-log-input.schema.json +55 -0
  118. package/schemas/event-feature-manifest.schema.json +1 -1
  119. package/schemas/focus-plan.schema.json +1 -1
  120. package/schemas/focus-planning-sidecar.schema.json +1 -1
  121. package/schemas/gui-animation-source.schema.json +1 -1
  122. package/schemas/gui-helper.schema.json +1 -1
  123. package/schemas/gui-scenario.schema.json +1 -1
  124. package/schemas/helper-expansion-request.schema.json +1 -1
  125. package/schemas/helper-expansion-summary.schema.json +1 -1
  126. package/schemas/impact-inspect-input.schema.json +1 -1
  127. package/schemas/map-operation.schema.json +597 -126
  128. package/schemas/mechanic-test-input.schema.json +1 -1
  129. package/schemas/mod-index-input.schema.json +51 -0
  130. package/schemas/operation-result.schema.json +2 -2
  131. package/schemas/package-check-input.schema.json +1 -1
  132. package/schemas/package-manifest.schema.json +1 -1
  133. package/schemas/probability-analysis-result.schema.json +1 -1
  134. package/schemas/probability-compare-input.schema.json +1 -1
  135. package/schemas/probability-evaluate-input.schema.json +1 -1
  136. package/schemas/probability-inspect-input.schema.json +1 -1
  137. package/schemas/probability-render-input.schema.json +1 -1
  138. package/schemas/probability-scenario-set.schema.json +1 -1
  139. package/schemas/probability-sequence-input.schema.json +1 -1
  140. package/schemas/probability-simulate-input.schema.json +1 -1
  141. package/schemas/probability-sweep-input.schema.json +1 -1
  142. package/schemas/reference-context-input.schema.json +1 -1
  143. package/schemas/reference-read-input.schema.json +1 -1
  144. package/schemas/reference-search-input.schema.json +1 -1
  145. package/schemas/scenario-suite.schema.json +3 -5
  146. package/schemas/scenario-test-input.schema.json +3 -5
  147. package/schemas/script-validate-input.schema.json +1 -1
  148. package/schemas/shared-scenario.schema.json +1 -1
  149. package/schemas/source-lookup-input.schema.json +1 -1
  150. package/server.json +3 -3
@@ -1,9 +1,19 @@
1
1
  import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
2
2
  import { probabilityPrompt, probabilityPromptArguments, probabilityPromptDefinition, } from '../prompts/probability.js';
3
3
  import { z } from 'zod/v4';
4
- import { probabilityCompareRequestSchema, probabilityEvaluateRequestSchema, probabilityInspectRequestSchema, probabilityRenderRequestSchema, probabilitySequenceRequestSchema, probabilitySimulateRequestSchema, probabilitySweepRequestSchema, } from '../../schemas/probability-requests.js';
4
+ import { probabilityAnalyzeRequestSchema, probabilityInspectRequestSchema, } from '../../schemas/probability-requests.js';
5
5
  import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
6
6
  import { strictOperationResultSchema } from '../server/result.js';
7
+ const rankedCandidateSchema = z
8
+ .object({
9
+ id: z.string().max(512),
10
+ eligible: z.enum(['true', 'false', 'unresolved']),
11
+ weight: z.number().nullable(),
12
+ weightRange: z.array(z.number().nullable()).length(2).optional(),
13
+ probability: z.number().nullable(),
14
+ mtthDays: z.number().nullable().optional(),
15
+ })
16
+ .strict();
7
17
  const analysisDataSchema = z
8
18
  .object({
9
19
  operation: z.enum(['evaluate', 'sweep', 'simulate', 'sequence', 'compare', 'render']),
@@ -30,6 +40,29 @@ const analysisDataSchema = z
30
40
  .optional(),
31
41
  comparisonChanges: nonNegativeIntegerSchema.optional(),
32
42
  visualResources: nonNegativeIntegerSchema.optional(),
43
+ ranking: z
44
+ .array(z
45
+ .object({
46
+ scenarioId: z.string().max(512),
47
+ poolComplete: z.boolean(),
48
+ candidates: z.array(rankedCandidateSchema).max(12),
49
+ omittedCandidates: nonNegativeIntegerSchema.optional(),
50
+ })
51
+ .strict())
52
+ .max(4),
53
+ missingInputs: z.array(z.string().max(1024)).max(16),
54
+ changes: z
55
+ .array(z
56
+ .object({
57
+ scenarioId: z.string().max(512),
58
+ candidateId: z.string().max(512),
59
+ weightDelta: z.number().nullable().optional(),
60
+ probabilityDelta: z.number().nullable().optional(),
61
+ eligibility: z.string().max(64).optional(),
62
+ })
63
+ .strict())
64
+ .max(12)
65
+ .optional(),
33
66
  })
34
67
  .strict();
35
68
  const inspectDataSchema = z
@@ -66,11 +99,12 @@ const inspectDataSchema = z
66
99
  requiredInputs: nonNegativeIntegerSchema,
67
100
  requiredInputPaths: z.array(z.string().max(1024)).max(32),
68
101
  requiredInputPathsTruncated: z.boolean(),
102
+ adapterRequiredInputPaths: z.array(z.string().max(1024)).max(32).optional(),
69
103
  unresolved: nonNegativeIntegerSchema,
70
104
  })
71
105
  .strict();
72
106
  const analysisOutput = strictOperationResultSchema(analysisDataSchema);
73
- const inspectOutput = strictOperationResultSchema(inspectDataSchema);
107
+ const inspectOutput = strictOperationResultSchema(z.union([analysisDataSchema, inspectDataSchema]));
74
108
  const readOnly = {
75
109
  readOnlyHint: true,
76
110
  destructiveHint: false,
@@ -80,57 +114,17 @@ const readOnly = {
80
114
  export const probabilityTaskTools = [
81
115
  {
82
116
  name: 'hoi4.probability_inspect',
83
- title: 'Inspect AI and MTTH weighted logic',
84
- description: 'Discover weighted adapters, candidates, provenance, and required inputs.',
117
+ title: 'Inspect and evaluate AI weights and MTTH',
118
+ description: 'Find weighted logic (ai_will_do, ai_chance, MTTH, random_list, strategy factors) by path, ID or candidate IDs and evaluate it under declared scenarios, or none: weights, probabilities and the facts still missing. Example: {"candidatePool":["my_decision"],"scenarioSet":[{"actor":"GER","state":{"has_war":false}}]}. Ranks the strongest candidates inline.',
85
119
  inputSchema: probabilityInspectRequestSchema,
86
120
  outputSchema: inspectOutput,
87
121
  annotations: readOnly,
88
122
  },
89
123
  {
90
- name: 'hoi4.probability_evaluate',
91
- title: 'Evaluate AI and MTTH scenarios',
92
- description: 'Evaluate weights and probabilities under explicit scenarios.',
93
- inputSchema: probabilityEvaluateRequestSchema,
94
- outputSchema: analysisOutput,
95
- annotations: readOnly,
96
- },
97
- {
98
- name: 'hoi4.probability_sweep',
99
- title: 'Sweep AI and MTTH parameters',
100
- description: 'Evaluate declared ranges, breakpoints, and rank reversals.',
101
- inputSchema: probabilitySweepRequestSchema,
102
- outputSchema: analysisOutput,
103
- annotations: readOnly,
104
- },
105
- {
106
- name: 'hoi4.probability_simulate',
107
- title: 'Simulate uncertain weighted scenarios',
108
- description: 'Run seeded simulations with distributions and confidence intervals.',
109
- inputSchema: probabilitySimulateRequestSchema,
110
- outputSchema: analysisOutput,
111
- annotations: readOnly,
112
- },
113
- {
114
- name: 'hoi4.probability_sequence',
115
- title: 'Analyze a declared weighted sequence',
116
- description: 'Analyze a manifest-declared dynamic weighted sequence.',
117
- inputSchema: probabilitySequenceRequestSchema,
118
- outputSchema: analysisOutput,
119
- annotations: readOnly,
120
- },
121
- {
122
- name: 'hoi4.probability_compare',
123
- title: 'Compare weighted source patches',
124
- description: 'Compare sources or declared pools under identical scenarios.',
125
- inputSchema: probabilityCompareRequestSchema,
126
- outputSchema: analysisOutput,
127
- annotations: readOnly,
128
- },
129
- {
130
- name: 'hoi4.probability_render',
131
- title: 'Render AI and MTTH analysis',
132
- description: 'Render retained probability analysis as deterministic resources.',
133
- inputSchema: probabilityRenderRequestSchema,
124
+ name: 'hoi4.probability_analyze',
125
+ title: 'Compare, sweep, simulate, or sequence weighted logic',
126
+ description: 'Deeper weighted analysis: compare before and after source under the same scenarios, sweep input ranges for breakpoints and rank reversals, simulate uncertain inputs, run a declared pool over time, or render a retained analysis. Example: {"analysis":"sweep","source":"common/decisions/my_decisions.txt","candidatePool":["my_decision"],"scenarioSet":[{"actor":"GER","state":{"variable.pressure":10}}],"sweep":{"paths":["variable.pressure"],"steps":10}}.',
127
+ inputSchema: probabilityAnalyzeRequestSchema,
134
128
  outputSchema: analysisOutput,
135
129
  annotations: readOnly,
136
130
  },
@@ -1,6 +1,7 @@
1
1
  import { errorResult, strictOperationResultSchema } from '../../core/operation-result.js';
2
2
  import { ReferenceToolService } from '../../reference/operations.js';
3
3
  import { referenceContextRequestSchema, referenceReadRequestSchema, referenceSearchRequestSchema, sourceLookupRequestSchema, referenceContextDataSchema, referenceReadDataSchema, referenceSearchDataSchema, sourceLookupDataSchema, } from '../../schemas/reference.js';
4
+ import { errorLogDataSchema, errorLogRequestSchema, modIndexDataSchema, modIndexRequestSchema, } from '../../schemas/mod-tools.js';
4
5
  import { scriptValidateDataSchema, scriptValidateRequestSchema, } from '../../schemas/script-validation.js';
5
6
  const readOnly = {
6
7
  readOnlyHint: true,
@@ -12,7 +13,7 @@ export const referenceTools = [
12
13
  {
13
14
  name: 'hoi4.reference_search',
14
15
  title: 'Search local HOI4 references',
15
- description: 'Search bounded, cited sections of the configured offline wiki and installed game documentation.',
16
+ description: 'Search the offline wiki and installed game documentation by words or script names; each hit gives its file, lines, revision and answering line. Example: {"query":"add_timed_idea"}. Returns 6 hits by default (up to 12); read one with hoi4.reference_read using its id and revision.',
16
17
  inputSchema: referenceSearchRequestSchema,
17
18
  outputSchema: strictOperationResultSchema(referenceSearchDataSchema),
18
19
  annotations: readOnly,
@@ -20,7 +21,7 @@ export const referenceTools = [
20
21
  {
21
22
  name: 'hoi4.reference_read',
22
23
  title: 'Read cited HOI4 reference section',
23
- description: 'Read at most 80 lines from one revision-bound local documentation section.',
24
+ description: 'Read one cited documentation section: pass the id and revision of a reference_search or reference_context hit. Example: {"id":"<hit id>","revision":"<hit revision>"}. Returns at most 80 lines (40 by default) and 8,000 bytes, with a line to continue from.',
24
25
  inputSchema: referenceReadRequestSchema,
25
26
  outputSchema: strictOperationResultSchema(referenceReadDataSchema),
26
27
  annotations: readOnly,
@@ -28,7 +29,7 @@ export const referenceTools = [
28
29
  {
29
30
  name: 'hoi4.reference_context',
30
31
  title: 'Get HOI4 task references',
31
- description: 'Return a compact bundle of offline wiki and installed game documentation citations for a modding surface.',
32
+ description: 'Cited wiki and installed-documentation sections for one modding surface (event, decision, idea, focus, technology, gui, map, localisation, ai, general). Example: {"surface":"event","question":"fire an event after a delay"}. Returns up to 16 short sections; read more with hoi4.reference_read.',
32
33
  inputSchema: referenceContextRequestSchema,
33
34
  outputSchema: strictOperationResultSchema(referenceContextDataSchema),
34
35
  annotations: readOnly,
@@ -36,7 +37,7 @@ export const referenceTools = [
36
37
  {
37
38
  name: 'hoi4.source_lookup',
38
39
  title: 'Look up HOI4 source symbol',
39
- description: 'Find exact Clausewitz definitions and usages; navigate nested keyPath blocks or list bounded child structure with view=structure. Or pass path and line to get the definitions and keyPath containing that line.',
40
+ description: 'Find where a script symbol is defined across game, DLC and mod, and where it is used; or pass path and line for the definition containing that line. Example: {"symbol":"my_scripted_effect"}. Returns up to 3 definitions with source text and 10 usages; keyPath and view=structure navigate nested blocks.',
40
41
  inputSchema: sourceLookupRequestSchema,
41
42
  outputSchema: strictOperationResultSchema(sourceLookupDataSchema),
42
43
  annotations: readOnly,
@@ -44,11 +45,27 @@ export const referenceTools = [
44
45
  {
45
46
  name: 'hoi4.script_validate',
46
47
  title: 'Check HOI4 script commands and scopes',
47
- description: 'Check an effect or trigger snippet (source, kind, scope) or a whole mod file (path) for syntax, native command kinds and scopes against cited installed documentation. Unknown helpers and native argument blocks remain explicit.',
48
+ description: 'Check an effect or trigger snippet, or a whole mod file, against the installed documentation: syntax, command kinds and scopes. Unknown helpers and native argument blocks stay marked unresolved, never valid. Example: {"source":"add_political_power = 10","kind":"effect","scope":"country"} or {"path":"events/a.txt"}. Returns up to 12 findings.',
48
49
  inputSchema: scriptValidateRequestSchema,
49
50
  outputSchema: strictOperationResultSchema(scriptValidateDataSchema),
50
51
  annotations: readOnly,
51
52
  },
53
+ {
54
+ name: 'hoi4.error_log',
55
+ title: 'Read the game error log',
56
+ description: 'Group the game\'s logs/error.log from its last launch by category, with each entry\'s source file, line, layer and whether that file changed since; mod entries first. Example: {"scope":"mod","category":"effect"}. Returns at most 20 entries (limit 50) with fix hints and which entries are new since the previous launch.',
57
+ inputSchema: errorLogRequestSchema,
58
+ outputSchema: strictOperationResultSchema(errorLogDataSchema),
59
+ annotations: readOnly,
60
+ },
61
+ {
62
+ name: 'hoi4.mod_index',
63
+ title: 'Index mod definitions and free IDs',
64
+ description: 'overview: the mod descriptor, definition counts by kind, folders and event namespace ranges. next_id: the next free numbered ID for an event namespace or prefix across every layer. Example: {"mode":"next_id","kind":"event","namespace":"my_mod"}. Returns a compact summary; the first call indexes the mod, which takes seconds on a large one.',
65
+ inputSchema: modIndexRequestSchema,
66
+ outputSchema: strictOperationResultSchema(modIndexDataSchema),
67
+ annotations: readOnly,
68
+ },
52
69
  ];
53
70
  export function registerReferenceTools(server, engine, context) {
54
71
  const service = new ReferenceToolService(engine);
@@ -1,7 +1,7 @@
1
1
  import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
2
2
  import { z } from 'zod/v4';
3
3
  import { helperExpansionRequestSchema, helperExpansionSummarySchema, } from '../../schemas/helper-expansion.js';
4
- import { technologyAnalysisModeSchema, technologyCompareRequestSchema, technologyGraphReferenceSchema, technologyImpactSchema, technologyInspectRequestSchema, technologyProposedSourceSchema, technologyRenderRequestSchema, validateTechnologyRenderRequest, technologyRenderViewSchema, validateTechnologyCompareRequest, validateTechnologyInspectRequest, } from '../../schemas/technology.js';
4
+ import { technologyAnalysisModeSchema, technologyGraphReferenceSchema, technologyImpactSchema, technologyInspectRequestSchema, technologyProposedSourceSchema, technologyRenderRequestSchema, validateTechnologyRenderRequest, technologyRenderViewSchema, validateTechnologyInspectToolRequest, } from '../../schemas/technology.js';
5
5
  import { compactValidatedInputSchema } from '../server/context-schemas.js';
6
6
  import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
7
7
  import { strictOperationResultSchema } from '../server/result.js';
@@ -10,26 +10,20 @@ const inspectInputSchema = z
10
10
  ...technologyInspectRequestSchema.shape,
11
11
  helperExpansion: compactValidatedInputSchema(helperExpansionRequestSchema, 'Bounded helper paths; opaque continuationUri resumes the same source and roots. See docs/technology.md.').optional(),
12
12
  impact: compactValidatedInputSchema(technologyImpactSchema, 'Rename or removal subject.').optional(),
13
- })
14
- .strict()
15
- .superRefine(validateTechnologyInspectRequest);
16
- const renderInputSchema = z
17
- .object({ ...technologyRenderRequestSchema.shape })
18
- .strict()
19
- .superRefine(validateTechnologyRenderRequest);
20
- const compareInputSchema = z
21
- .object({
22
- ...technologyCompareRequestSchema.shape,
23
- before: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Revision or graph resource.').optional(),
24
- after: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Revision or graph resource.').optional(),
13
+ before: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Mode compare: revision or graph resource.').optional(),
14
+ after: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Mode compare: revision or graph resource.').optional(),
25
15
  proposedSources: z
26
- .array(compactValidatedInputSchema(technologyProposedSourceSchema, 'In-memory source overlay.'))
16
+ .array(compactValidatedInputSchema(technologyProposedSourceSchema, 'Mode compare: in-memory source overlay.'))
27
17
  .min(1)
28
18
  .max(128)
29
19
  .optional(),
30
20
  })
31
21
  .strict()
32
- .superRefine(validateTechnologyCompareRequest);
22
+ .superRefine(validateTechnologyInspectToolRequest);
23
+ const renderInputSchema = z
24
+ .object({ ...technologyRenderRequestSchema.shape })
25
+ .strict()
26
+ .superRefine(validateTechnologyRenderRequest);
33
27
  const countsSchema = z
34
28
  .object({
35
29
  technologies: nonNegativeIntegerSchema,
@@ -44,7 +38,7 @@ const countsSchema = z
44
38
  artifacts: nonNegativeIntegerSchema,
45
39
  })
46
40
  .strict();
47
- const analysisOutputSchema = strictOperationResultSchema(z
41
+ const analysisDataSchema = z
48
42
  .object({
49
43
  mode: technologyAnalysisModeSchema,
50
44
  revision: sha256Schema,
@@ -52,7 +46,7 @@ const analysisOutputSchema = strictOperationResultSchema(z
52
46
  counts: countsSchema,
53
47
  helperExpansion: helperExpansionSummarySchema.optional(),
54
48
  })
55
- .strict());
49
+ .strict();
56
50
  const renderOutputSchema = strictOperationResultSchema(z
57
51
  .object({
58
52
  view: technologyRenderViewSchema,
@@ -72,7 +66,7 @@ const renderOutputSchema = strictOperationResultSchema(z
72
66
  sourceAccurate: z.boolean(),
73
67
  })
74
68
  .strict());
75
- const compareOutputSchema = strictOperationResultSchema(z
69
+ const compareDataSchema = z
76
70
  .object({
77
71
  beforeRevision: sha256Schema,
78
72
  afterRevision: sha256Schema,
@@ -87,7 +81,8 @@ const compareOutputSchema = strictOperationResultSchema(z
87
81
  .strict()
88
82
  .optional(),
89
83
  })
90
- .strict());
84
+ .strict();
85
+ const analysisOutputSchema = strictOperationResultSchema(z.union([analysisDataSchema, compareDataSchema]));
91
86
  const readOnlyTechnologyTool = {
92
87
  readOnlyHint: true,
93
88
  destructiveHint: false,
@@ -98,7 +93,7 @@ export const technologyTaskTools = [
98
93
  {
99
94
  name: 'hoi4.tech_inspect',
100
95
  title: 'Inspect technology trees',
101
- description: 'Scan, discover folders, trace, explain, inspect unlocks or bonuses, lint, and assess impact.',
96
+ description: 'Scan, trace, explain, list unlocks and bonus coverage, lint, assess impact or compare (mode compare) technology and doctrine graphs. Example: {"mode":"explain","technologyId":"my_tech"}. Returns counts and the main findings inline; the full report is a linked resource.',
102
97
  inputSchema: inspectInputSchema,
103
98
  outputSchema: analysisOutputSchema,
104
99
  annotations: readOnlyTechnologyTool,
@@ -106,19 +101,11 @@ export const technologyTaskTools = [
106
101
  {
107
102
  name: 'hoi4.tech_render',
108
103
  title: 'Render technology trees',
109
- description: 'Render source-linked JSON, SVG, PNG, and optional HTML technology views.',
104
+ description: 'Render a technology folder, dependencies, doctrine, bonuses, grants, unlocks or unresolved view as JSON, SVG and PNG with real item sizes and year guides. Example: {"view":"folder","folderId":"my_folder"}. Images are linked artifacts.',
110
105
  inputSchema: renderInputSchema,
111
106
  outputSchema: renderOutputSchema,
112
107
  annotations: readOnlyTechnologyTool,
113
108
  },
114
- {
115
- name: 'hoi4.tech_compare',
116
- title: 'Compare technology trees',
117
- description: 'Compare cached, resource-backed, current, or proposed source graphs without writes.',
118
- inputSchema: compareInputSchema,
119
- outputSchema: compareOutputSchema,
120
- annotations: readOnlyTechnologyTool,
121
- },
122
109
  ];
123
110
  export function registerTechnologyTools(server) {
124
111
  registerLegacyTaskTools(server, technologyTaskTools);
@@ -54,10 +54,18 @@ export function rateLimitClientAddress(request, configuration) {
54
54
  const forwarded = request.headers['x-forwarded-for'];
55
55
  if (typeof forwarded !== 'string')
56
56
  return undefined;
57
- const client = forwarded.split(',')[0]?.trim();
58
- if (client === undefined || isIP(client) === 0)
59
- return undefined;
60
- return normalizeIpAddress(client);
57
+ // Proxies append to the right, so only the entries after the last untrusted hop are
58
+ // trustworthy; the leftmost entry is whatever the client sent.
59
+ const hops = forwarded.split(',').map((hop) => hop.trim());
60
+ for (let index = hops.length - 1; index >= 0; index -= 1) {
61
+ const hop = hops[index];
62
+ if (isIP(hop) === 0)
63
+ return undefined;
64
+ const normalized = normalizeIpAddress(hop);
65
+ if (!trusted.has(normalized))
66
+ return normalized;
67
+ }
68
+ return undefined;
61
69
  }
62
70
  export function requiredScopesForMcpRequest(configuration, body) {
63
71
  const scopes = [...(configuration.http.oauth?.requiredScopes ?? ['hoi4:read'])];
@@ -502,6 +510,22 @@ export async function startHttpServer(engine, configuration, serverFactory) {
502
510
  // Supplying a defined parsed body to handleRequest prevents the SDK from falling back to
503
511
  // an unbounded Request.json() parse for alternate media types or empty established-session
504
512
  // requests. GET and DELETE deliberately remain bodyless.
513
+ // Authenticate before reading the body, so an unauthenticated caller cannot hold a request
514
+ // slot and body budget while it sends one slowly. Scopes that depend on the parsed body are
515
+ // checked again afterwards.
516
+ app.post('/mcp', async (request, response, next) => {
517
+ const baseScopes = requiredScopesForMcpRequest(configuration, undefined);
518
+ try {
519
+ const authorization = request.headers.authorization;
520
+ await authenticate(Array.isArray(authorization) ? undefined : authorization, configuration, baseScopes);
521
+ next();
522
+ }
523
+ catch (error) {
524
+ sendAuthError(response, configuration, error instanceof HttpAuthError
525
+ ? error
526
+ : new HttpAuthError(401, 'AUTH_INVALID', 'Authentication failed', 'invalid_token', baseScopes));
527
+ }
528
+ });
505
529
  app.post('/mcp', express.json({
506
530
  limit: http.maxBodyBytes,
507
531
  strict: true,
@@ -10,7 +10,9 @@ const PARENT_POLL_MS = 5_000;
10
10
  export { processAlive };
11
11
  export class StdioLifetime {
12
12
  #options;
13
- #open = new Set();
13
+ /** Open client requests and when each arrived. */
14
+ #open = new Map();
15
+ #parentGone = false;
14
16
  #lastActivity;
15
17
  #timer;
16
18
  #stopped = false;
@@ -40,7 +42,11 @@ export class StdioLifetime {
40
42
  this.#lastActivity = this.#options.now();
41
43
  const id = requestId(message, 'method');
42
44
  if (id !== undefined)
43
- this.#open.add(id);
45
+ this.#open.set(id, this.#lastActivity);
46
+ // A cancelled request gets no response, so the cancellation closes it.
47
+ const cancelled = cancelledRequestId(message);
48
+ if (cancelled !== undefined)
49
+ this.#open.delete(cancelled);
44
50
  }
45
51
  /** A server message: a response closes its request. */
46
52
  sent(message) {
@@ -56,21 +62,38 @@ export class StdioLifetime {
56
62
  check() {
57
63
  if (this.#stopped)
58
64
  return;
59
- const { parentPid, isAlive, idleExitMinutes, now, onExit } = this.#options;
60
- // A parent pid of 0 or 1 means the server was already reparented at start.
61
- if (parentPid > 1 && !isAlive(parentPid)) {
65
+ const { parentPid, isAlive, idleExitMinutes, now, onExit, pollMs } = this.#options;
66
+ const idleMs = idleExitMinutes * 60_000;
67
+ // A request open for twice the idle limit was abandoned by its client.
68
+ if (idleMs > 0)
69
+ for (const [id, openedAt] of this.#open)
70
+ if (now() - openedAt > 2 * idleMs)
71
+ this.#open.delete(id);
72
+ // A parent pid of 0 or 1 means the server was already reparented at start. Once the
73
+ // parent is gone, the server finishes open requests and waits for a quiet poll first,
74
+ // since some launchers exit while the client keeps using the pipes.
75
+ if (parentPid > 1 && !this.#parentGone && !isAlive(parentPid))
76
+ this.#parentGone = true;
77
+ if (this.#parentGone && this.#open.size === 0 && now() - this.#lastActivity >= pollMs) {
62
78
  this.stop();
63
79
  onExit('parent_exited');
64
80
  return;
65
81
  }
66
- if (idleExitMinutes > 0 &&
67
- this.#open.size === 0 &&
68
- now() - this.#lastActivity > idleExitMinutes * 60_000) {
82
+ if (idleMs > 0 && this.#open.size === 0 && now() - this.#lastActivity > idleMs) {
69
83
  this.stop();
70
84
  onExit('idle');
71
85
  }
72
86
  }
73
87
  }
88
+ function cancelledRequestId(message) {
89
+ if (typeof message !== 'object' || message === null)
90
+ return undefined;
91
+ const { method, params } = message;
92
+ if (method !== 'notifications/cancelled')
93
+ return undefined;
94
+ const id = params?.requestId;
95
+ return typeof id === 'string' || typeof id === 'number' ? id : undefined;
96
+ }
74
97
  function requestId(message, field) {
75
98
  if (typeof message !== 'object' || message === null || !(field in message))
76
99
  return undefined;
@@ -2,8 +2,8 @@ import { z } from 'zod/v4';
2
2
  import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
3
3
  import { errorResult, toolResult } from '../core/operation-result.js';
4
4
  import { ServiceError } from '../core/result.js';
5
- import { probabilityCompareRequestSchema, probabilityEvaluateRequestSchema, probabilityInspectRequestSchema, probabilityRenderRequestSchema, probabilitySequenceRequestSchema, probabilitySimulateRequestSchema, probabilitySweepRequestSchema, } from '../schemas/probability-requests.js';
6
- import { compareProbabilities, evaluateProbabilities, inspectProbabilities, normalizeProbabilityCompareRequest, normalizeProbabilityEvaluateRequest, normalizeProbabilityInspectRequest, normalizeProbabilityRenderRequest, normalizeProbabilitySequenceRequest, normalizeProbabilitySimulateRequest, normalizeProbabilitySweepRequest, renderProbabilities, sequenceProbabilities, simulateProbabilities, sweepProbabilities, } from './operations.js';
5
+ import { probabilityAnalyzeRequestSchema, probabilityInspectRequestSchema, } from '../schemas/probability-requests.js';
6
+ import { analyzeProbabilities, inspectProbabilities, normalizeProbabilityAnalyzeRequest, normalizeProbabilityInspectRequest, } from './operations.js';
7
7
  import { ProbabilityAnalyzer } from './service.js';
8
8
  const wireResult = z.record(z.string(), z.json());
9
9
  /** Registers probability operations without invoking transport handlers. */
@@ -48,11 +48,6 @@ export function registerProbabilityJobs(operations, engine) {
48
48
  }
49
49
  });
50
50
  }
51
- register('hoi4.probability_inspect', probabilityInspectRequestSchema, normalizeProbabilityInspectRequest, inspectProbabilities, 'Inspecting weighted source');
52
- register('hoi4.probability_evaluate', probabilityEvaluateRequestSchema, normalizeProbabilityEvaluateRequest, evaluateProbabilities, 'Evaluating weighted scenarios');
53
- register('hoi4.probability_sweep', probabilitySweepRequestSchema, normalizeProbabilitySweepRequest, sweepProbabilities, 'Evaluating parameter sweep');
54
- register('hoi4.probability_simulate', probabilitySimulateRequestSchema, normalizeProbabilitySimulateRequest, simulateProbabilities, 'Sampling uncertain scenarios');
55
- register('hoi4.probability_sequence', probabilitySequenceRequestSchema, normalizeProbabilitySequenceRequest, sequenceProbabilities, 'Analyzing declared sequence');
56
- register('hoi4.probability_compare', probabilityCompareRequestSchema, normalizeProbabilityCompareRequest, compareProbabilities, 'Comparing weighted source');
57
- register('hoi4.probability_render', probabilityRenderRequestSchema, normalizeProbabilityRenderRequest, renderProbabilities, 'Rendering analysis resources');
51
+ register('hoi4.probability_inspect', probabilityInspectRequestSchema, normalizeProbabilityInspectRequest, inspectProbabilities, 'Inspecting and evaluating weighted source');
52
+ register('hoi4.probability_analyze', probabilityAnalyzeRequestSchema, normalizeProbabilityAnalyzeRequest, analyzeProbabilities, 'Analyzing weighted source');
58
53
  }