hoi4-agent-tools 3.9.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 (157) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +115 -147
  3. package/dist/bin/http.js +12 -0
  4. package/dist/bin/setup.js +3 -2
  5. package/dist/bin/stdio.js +12 -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 +56 -10
  13. package/dist/hoi4_agent_tools/core/index.js +45 -20
  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 +155 -0
  24. package/dist/hoi4_agent_tools/core/server-status-resource.js +22 -0
  25. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
  26. package/dist/hoi4_agent_tools/core/source/cache.js +108 -29
  27. package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
  28. package/dist/hoi4_agent_tools/core/transactions.js +114 -23
  29. package/dist/hoi4_agent_tools/core/workspace.js +84 -8
  30. package/dist/hoi4_agent_tools/decision/ai.js +1 -1
  31. package/dist/hoi4_agent_tools/decision/service.js +28 -0
  32. package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
  33. package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
  34. package/dist/hoi4_agent_tools/event/operations.js +14 -1
  35. package/dist/hoi4_agent_tools/event/service.js +7 -0
  36. package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
  37. package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
  38. package/dist/hoi4_agent_tools/gui/scenario-generator.js +11 -3
  39. package/dist/hoi4_agent_tools/gui/studio.js +20 -7
  40. package/dist/hoi4_agent_tools/gui/validators.js +8 -6
  41. package/dist/hoi4_agent_tools/map/bmp.js +1 -1
  42. package/dist/hoi4_agent_tools/map/derived.js +748 -0
  43. package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
  44. package/dist/hoi4_agent_tools/map/model.js +103 -0
  45. package/dist/hoi4_agent_tools/map/operations.js +427 -58
  46. package/dist/hoi4_agent_tools/map/references.js +483 -0
  47. package/dist/hoi4_agent_tools/map/service.js +136 -61
  48. package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
  49. package/dist/hoi4_agent_tools/map/validation.js +3 -0
  50. package/dist/hoi4_agent_tools/map/world.js +1280 -0
  51. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +13 -1
  52. package/dist/hoi4_agent_tools/mcp/resources/register.js +6 -0
  53. package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
  54. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
  55. package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
  56. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
  57. package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
  58. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +25 -5
  59. package/dist/hoi4_agent_tools/mcp/tools/event.js +18 -29
  60. package/dist/hoi4_agent_tools/mcp/tools/focus.js +4 -12
  61. package/dist/hoi4_agent_tools/mcp/tools/gui.js +3 -3
  62. package/dist/hoi4_agent_tools/mcp/tools/job.js +2 -2
  63. package/dist/hoi4_agent_tools/mcp/tools/map.js +13 -16
  64. package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
  65. package/dist/hoi4_agent_tools/mcp/tools/reference.js +22 -5
  66. package/dist/hoi4_agent_tools/mcp/tools/technology.js +16 -26
  67. package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
  68. package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +37 -18
  69. package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
  70. package/dist/hoi4_agent_tools/probability/operations.js +175 -13
  71. package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
  72. package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
  73. package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
  74. package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
  75. package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
  76. package/dist/hoi4_agent_tools/reference/operations.js +88 -19
  77. package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
  78. package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
  79. package/dist/hoi4_agent_tools/reference/service.js +6 -0
  80. package/dist/hoi4_agent_tools/schemas/event.js +78 -19
  81. package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
  82. package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
  83. package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
  84. package/dist/hoi4_agent_tools/schemas/map.js +122 -3
  85. package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
  86. package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
  87. package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
  88. package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
  89. package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
  90. package/dist/hoi4_agent_tools/schemas/technology.js +83 -23
  91. package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
  92. package/dist/hoi4_agent_tools/technology/operations.js +14 -1
  93. package/dist/hoi4_agent_tools/technology/service.js +7 -0
  94. package/dist/hoi4_agent_tools/version.js +1 -1
  95. package/docs/README.md +7 -4
  96. package/docs/analysis.md +13 -0
  97. package/docs/configuration.md +281 -0
  98. package/docs/development.md +24 -2
  99. package/docs/events.md +27 -3
  100. package/docs/examples.md +6 -6
  101. package/docs/focus.md +125 -4
  102. package/docs/gui.md +98 -17
  103. package/docs/helper-expansion.md +8 -8
  104. package/docs/http.md +3 -1
  105. package/docs/jobs.md +19 -13
  106. package/docs/map.md +197 -6
  107. package/docs/mechanics.md +4 -0
  108. package/docs/probability.md +42 -15
  109. package/docs/reference.md +71 -4
  110. package/docs/setup.md +45 -11
  111. package/docs/technology.md +26 -1
  112. package/docs/third-party-notices.md +11 -0
  113. package/docs/tools.md +151 -42
  114. package/docs/troubleshooting.md +61 -0
  115. package/examples/clients/codex-config.toml +5 -2
  116. package/examples/clients/generic.json +2 -2
  117. package/examples/config/config.json +7 -0
  118. package/npm-shrinkwrap.json +161 -162
  119. package/package.json +13 -8
  120. package/schemas/configuration.schema.json +144 -128
  121. package/schemas/continuous-focus-palette.schema.json +1 -1
  122. package/schemas/custom-weighted-pool.schema.json +1 -1
  123. package/schemas/decision-inspect-input.schema.json +1 -1
  124. package/schemas/error-log-input.schema.json +55 -0
  125. package/schemas/event-feature-manifest.schema.json +1 -1
  126. package/schemas/focus-plan.schema.json +1 -1
  127. package/schemas/focus-planning-sidecar.schema.json +1 -1
  128. package/schemas/gui-animation-source.schema.json +1 -1
  129. package/schemas/gui-helper.schema.json +1 -1
  130. package/schemas/gui-scenario.schema.json +1 -1
  131. package/schemas/helper-expansion-request.schema.json +1 -1
  132. package/schemas/helper-expansion-summary.schema.json +1 -1
  133. package/schemas/impact-inspect-input.schema.json +1 -1
  134. package/schemas/map-operation.schema.json +597 -126
  135. package/schemas/mechanic-test-input.schema.json +1 -1
  136. package/schemas/mod-index-input.schema.json +51 -0
  137. package/schemas/operation-result.schema.json +2 -2
  138. package/schemas/package-check-input.schema.json +1 -1
  139. package/schemas/package-manifest.schema.json +1 -1
  140. package/schemas/probability-analysis-result.schema.json +1 -1
  141. package/schemas/probability-compare-input.schema.json +1 -1
  142. package/schemas/probability-evaluate-input.schema.json +1 -1
  143. package/schemas/probability-inspect-input.schema.json +1 -1
  144. package/schemas/probability-render-input.schema.json +1 -1
  145. package/schemas/probability-scenario-set.schema.json +1 -1
  146. package/schemas/probability-sequence-input.schema.json +1 -1
  147. package/schemas/probability-simulate-input.schema.json +1 -1
  148. package/schemas/probability-sweep-input.schema.json +1 -1
  149. package/schemas/reference-context-input.schema.json +1 -1
  150. package/schemas/reference-read-input.schema.json +1 -1
  151. package/schemas/reference-search-input.schema.json +1 -1
  152. package/schemas/scenario-suite.schema.json +3 -5
  153. package/schemas/scenario-test-input.schema.json +3 -5
  154. package/schemas/script-validate-input.schema.json +1 -1
  155. package/schemas/shared-scenario.schema.json +1 -1
  156. package/schemas/source-lookup-input.schema.json +1 -1
  157. package/server.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
2
2
  import { z } from 'zod/v4';
3
- import { mapInspectRequestSchema, mapRenderRequestSchema, mapRewriteRequestSchema, } from '../../schemas/map-requests.js';
3
+ import { mapInspectRequestSchema, mapRewriteRequestSchema } from '../../schemas/map-requests.js';
4
4
  import { allocationEvidenceSchema, bitmapRenderHashesSchema, nonNegativeIntegerSchema, operationBlockerDataSchema, sha256Schema, } from '../server/output-schemas.js';
5
5
  import { strictOperationResultSchema } from '../server/result.js';
6
6
  const changedBoundsSchema = z
@@ -55,7 +55,7 @@ const mapSemanticDiffSchema = z
55
55
  normalAdjacenciesChanged: z.boolean(),
56
56
  })
57
57
  .strict();
58
- const mapWorkspaceInspectOutputSchema = strictOperationResultSchema(z
58
+ const mapWorkspaceInspectDataSchema = z
59
59
  .object({
60
60
  revision: sha256Schema,
61
61
  sharedRevision: sha256Schema,
@@ -78,9 +78,13 @@ const mapWorkspaceInspectOutputSchema = strictOperationResultSchema(z
78
78
  lookupOnly: z.boolean().optional(),
79
79
  unknownProvinceIds: z.array(nonNegativeIntegerSchema).max(512),
80
80
  missingGeometryProvinceIds: z.array(nonNegativeIntegerSchema).max(512),
81
+ scriptReferences: z
82
+ .object({ references: nonNegativeIntegerSchema, missing: nonNegativeIntegerSchema })
83
+ .strict()
84
+ .optional(),
81
85
  })
82
- .strict());
83
- const mapRenderOutputSchema = strictOperationResultSchema(z
86
+ .strict();
87
+ const mapRenderDataSchema = z
84
88
  .object({
85
89
  revision: sha256Schema,
86
90
  width: nonNegativeIntegerSchema,
@@ -97,7 +101,8 @@ const mapRenderOutputSchema = strictOperationResultSchema(z
97
101
  .strict()
98
102
  .optional(),
99
103
  })
100
- .strict());
104
+ .strict();
105
+ const mapInspectOutputSchema = strictOperationResultSchema(z.union([mapWorkspaceInspectDataSchema, mapRenderDataSchema]));
101
106
  const mapPlanOutputSchema = strictOperationResultSchema(z
102
107
  .object({
103
108
  execution: z.enum(['applied', 'blocked', 'unchanged']),
@@ -123,23 +128,15 @@ export const mapTaskTools = [
123
128
  {
124
129
  name: 'hoi4.map_inspect',
125
130
  title: 'Inspect HOI4 map',
126
- description: 'Validate and inspect the complete map, or use lookupOnly for bounded coordinate and entity lookups.',
131
+ description: 'Validate the map with a searchable navigator, look up provinces, states, regions, names or coordinates (lookupOnly for speed), render a layer, tile or area, or check script references (references:true). Example: {"query":"Brandenburg","lookupOnly":true}. Returns counts and matches inline; PNG, HTML and the catalog are artifacts.',
127
132
  inputSchema: mapInspectRequestSchema,
128
- outputSchema: mapWorkspaceInspectOutputSchema,
129
- annotations: artifactProducing,
130
- },
131
- {
132
- name: 'hoi4.map_render',
133
- title: 'Render map inspection artifacts',
134
- description: 'Render searchable full-map PNG, JSON, and HTML, or a reusable source-coordinate tile.',
135
- inputSchema: mapRenderRequestSchema,
136
- outputSchema: mapRenderOutputSchema,
133
+ outputSchema: mapInspectOutputSchema,
137
134
  annotations: artifactProducing,
138
135
  },
139
136
  {
140
137
  name: 'hoi4.map_rewrite',
141
138
  title: 'Create or clean up map content',
142
- description: 'Apply map creation, edits, and ID swaps with visual and semantic evidence.',
139
+ description: 'Create a world, or create, split, merge, move and renumber states and provinces, regenerate positions and supply, and remap script references. Example: {"operations":[{"id":"s1","kind":"create_state","provinceIds":[120,121],"displayName":"New State"}]}. All or nothing: a failed operation leaves no partial files. Returns changed IDs, semantic diff and a render.',
143
140
  inputSchema: mapRewriteRequestSchema,
144
141
  outputSchema: mapPlanOutputSchema,
145
142
  annotations: {
@@ -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, 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,23 +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.object({ ...technologyRenderRequestSchema.shape }).strict();
17
- const compareInputSchema = z
18
- .object({
19
- ...technologyCompareRequestSchema.shape,
20
- before: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Revision or graph resource.').optional(),
21
- 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(),
22
15
  proposedSources: z
23
- .array(compactValidatedInputSchema(technologyProposedSourceSchema, 'In-memory source overlay.'))
16
+ .array(compactValidatedInputSchema(technologyProposedSourceSchema, 'Mode compare: in-memory source overlay.'))
24
17
  .min(1)
25
18
  .max(128)
26
19
  .optional(),
27
20
  })
28
21
  .strict()
29
- .superRefine(validateTechnologyCompareRequest);
22
+ .superRefine(validateTechnologyInspectToolRequest);
23
+ const renderInputSchema = z
24
+ .object({ ...technologyRenderRequestSchema.shape })
25
+ .strict()
26
+ .superRefine(validateTechnologyRenderRequest);
30
27
  const countsSchema = z
31
28
  .object({
32
29
  technologies: nonNegativeIntegerSchema,
@@ -41,7 +38,7 @@ const countsSchema = z
41
38
  artifacts: nonNegativeIntegerSchema,
42
39
  })
43
40
  .strict();
44
- const analysisOutputSchema = strictOperationResultSchema(z
41
+ const analysisDataSchema = z
45
42
  .object({
46
43
  mode: technologyAnalysisModeSchema,
47
44
  revision: sha256Schema,
@@ -49,7 +46,7 @@ const analysisOutputSchema = strictOperationResultSchema(z
49
46
  counts: countsSchema,
50
47
  helperExpansion: helperExpansionSummarySchema.optional(),
51
48
  })
52
- .strict());
49
+ .strict();
53
50
  const renderOutputSchema = strictOperationResultSchema(z
54
51
  .object({
55
52
  view: technologyRenderViewSchema,
@@ -69,7 +66,7 @@ const renderOutputSchema = strictOperationResultSchema(z
69
66
  sourceAccurate: z.boolean(),
70
67
  })
71
68
  .strict());
72
- const compareOutputSchema = strictOperationResultSchema(z
69
+ const compareDataSchema = z
73
70
  .object({
74
71
  beforeRevision: sha256Schema,
75
72
  afterRevision: sha256Schema,
@@ -84,7 +81,8 @@ const compareOutputSchema = strictOperationResultSchema(z
84
81
  .strict()
85
82
  .optional(),
86
83
  })
87
- .strict());
84
+ .strict();
85
+ const analysisOutputSchema = strictOperationResultSchema(z.union([analysisDataSchema, compareDataSchema]));
88
86
  const readOnlyTechnologyTool = {
89
87
  readOnlyHint: true,
90
88
  destructiveHint: false,
@@ -95,7 +93,7 @@ export const technologyTaskTools = [
95
93
  {
96
94
  name: 'hoi4.tech_inspect',
97
95
  title: 'Inspect technology trees',
98
- 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.',
99
97
  inputSchema: inspectInputSchema,
100
98
  outputSchema: analysisOutputSchema,
101
99
  annotations: readOnlyTechnologyTool,
@@ -103,19 +101,11 @@ export const technologyTaskTools = [
103
101
  {
104
102
  name: 'hoi4.tech_render',
105
103
  title: 'Render technology trees',
106
- 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.',
107
105
  inputSchema: renderInputSchema,
108
106
  outputSchema: renderOutputSchema,
109
107
  annotations: readOnlyTechnologyTool,
110
108
  },
111
- {
112
- name: 'hoi4.tech_compare',
113
- title: 'Compare technology trees',
114
- description: 'Compare cached, resource-backed, current, or proposed source graphs without writes.',
115
- inputSchema: compareInputSchema,
116
- outputSchema: compareOutputSchema,
117
- annotations: readOnlyTechnologyTool,
118
- },
119
109
  ];
120
110
  export function registerTechnologyTools(server) {
121
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,
@@ -5,20 +5,14 @@
5
5
  * pipe, and a client keeps an idle connection it will never use again. The first is detected
6
6
  * by polling the parent process; the second by an optional idle limit.
7
7
  */
8
+ import { processAlive } from '../../core/server-registry.js';
8
9
  const PARENT_POLL_MS = 5_000;
9
- /** Whether a process exists; a permission refusal still proves it exists. */
10
- export function processAlive(pid) {
11
- try {
12
- process.kill(pid, 0);
13
- return true;
14
- }
15
- catch (error) {
16
- return error.code === 'EPERM';
17
- }
18
- }
10
+ export { processAlive };
19
11
  export class StdioLifetime {
20
12
  #options;
21
- #open = new Set();
13
+ /** Open client requests and when each arrived. */
14
+ #open = new Map();
15
+ #parentGone = false;
22
16
  #lastActivity;
23
17
  #timer;
24
18
  #stopped = false;
@@ -48,7 +42,11 @@ export class StdioLifetime {
48
42
  this.#lastActivity = this.#options.now();
49
43
  const id = requestId(message, 'method');
50
44
  if (id !== undefined)
51
- 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);
52
50
  }
53
51
  /** A server message: a response closes its request. */
54
52
  sent(message) {
@@ -57,24 +55,45 @@ export class StdioLifetime {
57
55
  if (id !== undefined)
58
56
  this.#open.delete(id);
59
57
  }
58
+ /** When the client last sent or received a message, in epoch milliseconds. */
59
+ lastActivity() {
60
+ return this.#lastActivity;
61
+ }
60
62
  check() {
61
63
  if (this.#stopped)
62
64
  return;
63
- const { parentPid, isAlive, idleExitMinutes, now, onExit } = this.#options;
64
- // A parent pid of 0 or 1 means the server was already reparented at start.
65
- 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) {
66
78
  this.stop();
67
79
  onExit('parent_exited');
68
80
  return;
69
81
  }
70
- if (idleExitMinutes > 0 &&
71
- this.#open.size === 0 &&
72
- now() - this.#lastActivity > idleExitMinutes * 60_000) {
82
+ if (idleMs > 0 && this.#open.size === 0 && now() - this.#lastActivity > idleMs) {
73
83
  this.stop();
74
84
  onExit('idle');
75
85
  }
76
86
  }
77
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
+ }
78
97
  function requestId(message, field) {
79
98
  if (typeof message !== 'object' || message === null || !(field in message))
80
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
  }