hoi4-agent-tools 3.0.9 → 3.2.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 (145) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +10 -2
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
  6. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  7. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  8. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  9. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
  16. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  17. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  18. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  19. package/dist/hoi4_agent_tools/core/index.js +47 -4
  20. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  21. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  22. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  23. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  24. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  25. package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
  26. package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
  27. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  28. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  29. package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
  30. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  31. package/dist/hoi4_agent_tools/core/result.js +1 -0
  32. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  33. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  34. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  35. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  36. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  37. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  38. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  39. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  40. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  41. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  42. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  43. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  44. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  45. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  46. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  47. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  48. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  49. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  50. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  51. package/dist/hoi4_agent_tools/event/service.js +67 -15
  52. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  53. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  54. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  55. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  56. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  57. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  58. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  59. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  60. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  61. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  62. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  63. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  64. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  66. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  67. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  68. package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
  69. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  70. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  71. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  72. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  73. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  74. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
  75. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  76. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  77. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
  78. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  79. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  80. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  81. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  82. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  83. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  84. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  85. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  86. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  87. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  88. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  89. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  90. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  91. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  92. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  93. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  94. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  95. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  96. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  97. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  98. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  99. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  100. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  101. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  102. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  103. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  104. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  105. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  106. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  107. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  108. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  109. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  110. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  111. package/dist/hoi4_agent_tools/version.js +1 -1
  112. package/docs/README.md +3 -0
  113. package/docs/analysis.md +100 -0
  114. package/docs/development.md +2 -0
  115. package/docs/events.md +7 -2
  116. package/docs/helper-expansion.md +78 -0
  117. package/docs/http.md +2 -0
  118. package/docs/jobs.md +86 -0
  119. package/docs/technology.md +6 -2
  120. package/package.json +15 -6
  121. package/schemas/configuration.schema.json +1 -1
  122. package/schemas/continuous-focus-palette.schema.json +1 -1
  123. package/schemas/custom-weighted-pool.schema.json +1 -1
  124. package/schemas/decision-inspect-input.schema.json +290 -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 +44 -0
  132. package/schemas/helper-expansion-summary.schema.json +70 -0
  133. package/schemas/impact-inspect-input.schema.json +203 -0
  134. package/schemas/map-operation.schema.json +1 -1
  135. package/schemas/operation-result.schema.json +1 -1
  136. package/schemas/probability-analysis-result.schema.json +1 -1
  137. package/schemas/probability-compare-input.schema.json +1 -1
  138. package/schemas/probability-evaluate-input.schema.json +1 -1
  139. package/schemas/probability-inspect-input.schema.json +1 -1
  140. package/schemas/probability-render-input.schema.json +1 -1
  141. package/schemas/probability-scenario-set.schema.json +1 -1
  142. package/schemas/probability-sequence-input.schema.json +1 -1
  143. package/schemas/probability-simulate-input.schema.json +1 -1
  144. package/schemas/probability-sweep-input.schema.json +1 -1
  145. package/server.json +3 -3
@@ -1,83 +1,22 @@
1
+ import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
1
2
  import { z } from 'zod/v4';
2
- import { publicArtifactLink } from '../../core/artifacts.js';
3
- import { compareCodeUnits, hashCanonical } from '../../core/canonical.js';
4
- import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
5
- import { emptyServiceResult } from '../../core/result.js';
6
- import { workspaceIdSchema } from '../../schemas/common.js';
7
- import { technologyDefectClassSchema, technologyDirectionSchema, technologyGraphReferenceSchema, technologyIdSchema, technologyImpactSchema, technologyProposedSourceSchema, technologyRenderViewSchema, technologyUnlockKindSchema, } from '../../schemas/technology.js';
8
- import { TechnologyTreeViewer, technologyDiagnostics, } from '../../technology/service.js';
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';
9
5
  import { compactValidatedInputSchema } from '../server/context-schemas.js';
10
6
  import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
11
- import { progressReporter } from '../server/progress.js';
12
- import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
13
- import { resolveServerWorkspaceId } from '../server/base-tools.js';
14
- const technologyMode = z.enum([
15
- 'scan',
16
- 'folders',
17
- 'trace',
18
- 'explain',
19
- 'unlocks',
20
- 'bonus_coverage',
21
- 'lint',
22
- 'impact',
23
- ]);
24
- const inspectInput = z
7
+ import { strictOperationResultSchema } from '../server/result.js';
8
+ const inspectInputSchema = z
25
9
  .object({
26
- workspaceId: workspaceIdSchema,
27
- mode: technologyMode,
28
- folderId: technologyIdSchema.optional(),
29
- technologyId: technologyIdSchema.optional(),
30
- categoryId: technologyIdSchema.optional(),
31
- targetKind: technologyUnlockKindSchema.optional(),
32
- targetId: technologyIdSchema.optional(),
33
- direction: technologyDirectionSchema.optional(),
34
- maxDepth: z.number().int().min(1).max(256).optional(),
35
- maxNodes: z.number().int().min(1).max(25_000).optional(),
36
- includeSubTechnologies: z.boolean().optional(),
37
- classifications: z.array(technologyDefectClassSchema).max(4).optional(),
38
- codes: z.array(z.string().min(1).max(256)).max(100).optional(),
10
+ ...technologyInspectRequestSchema.shape,
11
+ helperExpansion: compactValidatedInputSchema(helperExpansionRequestSchema, 'Bounded helper paths; opaque continuationUri resumes the same source and roots. See docs/technology.md.').optional(),
39
12
  impact: compactValidatedInputSchema(technologyImpactSchema, 'Rename or removal subject.').optional(),
40
- refresh: z.boolean().optional(),
41
13
  })
42
14
  .strict()
43
- .superRefine((value, context) => {
44
- if ((value.mode === 'trace' || value.mode === 'explain') && value.technologyId === undefined)
45
- context.addIssue({
46
- code: 'custom',
47
- path: ['technologyId'],
48
- message: `${value.mode} requires technologyId`,
49
- });
50
- if (value.mode === 'impact' && value.impact === undefined)
51
- context.addIssue({
52
- code: 'custom',
53
- path: ['impact'],
54
- message: 'Impact mode requires impact',
55
- });
56
- });
57
- const renderInput = z
15
+ .superRefine(validateTechnologyInspectRequest);
16
+ const renderInputSchema = z.object({ ...technologyRenderRequestSchema.shape }).strict();
17
+ const compareInputSchema = z
58
18
  .object({
59
- workspaceId: workspaceIdSchema,
60
- view: technologyRenderViewSchema.exclude(['comparison']),
61
- folderId: technologyIdSchema.optional(),
62
- technologyId: technologyIdSchema.optional(),
63
- categoryId: technologyIdSchema.optional(),
64
- targetId: technologyIdSchema.optional(),
65
- maxNodes: z.number().int().min(1).max(2_000).optional(),
66
- includeHtml: z.boolean().optional(),
67
- refresh: z.boolean().optional(),
68
- })
69
- .strict()
70
- .superRefine((value, context) => {
71
- if (value.view === 'folder' && value.folderId === undefined)
72
- context.addIssue({
73
- code: 'custom',
74
- path: ['folderId'],
75
- message: 'Folder view requires folderId',
76
- });
77
- });
78
- const compareInput = z
79
- .object({
80
- workspaceId: workspaceIdSchema,
19
+ ...technologyCompareRequestSchema.shape,
81
20
  before: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Revision or graph resource.').optional(),
82
21
  after: compactValidatedInputSchema(technologyGraphReferenceSchema, 'Revision or graph resource.').optional(),
83
22
  proposedSources: z
@@ -85,19 +24,9 @@ const compareInput = z
85
24
  .min(1)
86
25
  .max(128)
87
26
  .optional(),
88
- render: z.boolean().optional(),
89
- maxRenderNodes: z.number().int().min(1).max(2_000).optional(),
90
- refresh: z.boolean().optional(),
91
27
  })
92
28
  .strict()
93
- .superRefine((value, context) => {
94
- if (value.after !== undefined && value.proposedSources !== undefined)
95
- context.addIssue({
96
- code: 'custom',
97
- path: ['after'],
98
- message: 'after conflicts with proposedSources',
99
- });
100
- });
29
+ .superRefine(validateTechnologyCompareRequest);
101
30
  const countsSchema = z
102
31
  .object({
103
32
  technologies: nonNegativeIntegerSchema,
@@ -112,15 +41,16 @@ const countsSchema = z
112
41
  artifacts: nonNegativeIntegerSchema,
113
42
  })
114
43
  .strict();
115
- const analysisOutput = strictOperationResultSchema(z
44
+ const analysisOutputSchema = strictOperationResultSchema(z
116
45
  .object({
117
- mode: technologyMode,
46
+ mode: technologyAnalysisModeSchema,
118
47
  revision: sha256Schema,
119
48
  graphHash: sha256Schema,
120
49
  counts: countsSchema,
50
+ helperExpansion: helperExpansionSummarySchema.optional(),
121
51
  })
122
52
  .strict());
123
- const renderOutput = strictOperationResultSchema(z
53
+ const renderOutputSchema = strictOperationResultSchema(z
124
54
  .object({
125
55
  view: technologyRenderViewSchema,
126
56
  revision: sha256Schema,
@@ -139,7 +69,7 @@ const renderOutput = strictOperationResultSchema(z
139
69
  sourceAccurate: z.boolean(),
140
70
  })
141
71
  .strict());
142
- const compareOutput = strictOperationResultSchema(z
72
+ const compareOutputSchema = strictOperationResultSchema(z
143
73
  .object({
144
74
  beforeRevision: sha256Schema,
145
75
  afterRevision: sha256Schema,
@@ -155,232 +85,38 @@ const compareOutput = strictOperationResultSchema(z
155
85
  .optional(),
156
86
  })
157
87
  .strict());
158
- const readOnly = {
88
+ const readOnlyTechnologyTool = {
159
89
  readOnlyHint: true,
160
90
  destructiveHint: false,
161
91
  idempotentHint: true,
162
92
  openWorldHint: false,
163
93
  };
164
- function graphHash(graph) {
165
- return hashCanonical({
166
- schemaVersion: graph.schemaVersion,
167
- analysisMode: graph.analysisMode ?? 'full',
168
- workspaceIdentity: graph.workspaceIdentity,
169
- revision: graph.revision,
170
- statistics: graph.statistics,
171
- sourceHashes: graph.sourceHashes,
172
- });
173
- }
174
- function counts(graph, artifacts) {
175
- return {
176
- technologies: graph.statistics.technologyCount,
177
- legacyDoctrines: graph.statistics.legacyDoctrineCount,
178
- folders: graph.statistics.folderCount,
179
- placements: graph.statistics.placementCount,
180
- edges: graph.edges.length,
181
- unlocks: graph.statistics.unlockCount,
182
- references: graph.statistics.externalReferenceCount,
183
- issues: graph.statistics.issueCount,
184
- unresolved: graph.statistics.unresolvedCount,
185
- artifacts,
186
- };
187
- }
188
- function validation(graph) {
189
- const blocking = technologyDiagnostics(graph).filter(({ severity }) => severity === 'error' || severity === 'blocker').length;
190
- return {
191
- passed: graph.complete && blocking === 0,
192
- checks: [
193
- {
194
- id: 'technology-analysis',
195
- passed: graph.complete && blocking === 0,
196
- message: graph.complete
197
- ? `${blocking} blocking technology diagnostics; full evidence is linked`
198
- : graph.analysisMode === 'focused'
199
- ? 'Helper projections were deferred for this large workspace; direct evidence is linked'
200
- : `${graph.skippedSourceCount} source(s) were skipped; full evidence is linked`,
201
- },
202
- ],
203
- };
204
- }
205
- function analysisRequest(input, workspaceId, context, signal) {
206
- const impact = input.impact === undefined
207
- ? undefined
208
- : {
209
- kind: input.impact.kind,
210
- id: input.impact.id,
211
- operation: input.impact.operation,
212
- ...(input.impact.replacementId === undefined
213
- ? {}
214
- : { replacementId: input.impact.replacementId }),
215
- };
216
- const values = {
217
- folderId: input.folderId,
218
- technologyId: input.technologyId,
219
- categoryId: input.categoryId,
220
- targetKind: input.targetKind,
221
- targetId: input.targetId,
222
- direction: input.direction,
223
- maxDepth: input.maxDepth,
224
- maxNodes: input.maxNodes,
225
- includeSubTechnologies: input.includeSubTechnologies,
226
- classifications: input.classifications,
227
- codes: input.codes,
228
- impact,
229
- refresh: input.refresh,
230
- };
231
- const compact = Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
232
- return {
233
- workspaceId,
234
- mode: input.mode,
235
- ...compact,
236
- ...(context.principal === undefined ? {} : { principal: context.principal }),
237
- signal,
238
- };
239
- }
240
- export function registerTechnologyTools(server, engine, context) {
241
- const viewer = new TechnologyTreeViewer(engine);
242
- const cacheLifetime = new IdleCacheLifetime(() => viewer.clearCaches());
243
- server.registerTool('hoi4.tech_inspect', {
94
+ export const technologyTaskTools = [
95
+ {
96
+ name: 'hoi4.tech_inspect',
244
97
  title: 'Inspect technology trees',
245
98
  description: 'Scan, discover folders, trace, explain, inspect unlocks or bonuses, lint, and assess impact.',
246
- inputSchema: inspectInput,
247
- outputSchema: analysisOutput,
248
- annotations: readOnly,
249
- }, async (input, extra) => {
250
- const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
251
- const releaseCaches = cacheLifetime.begin();
252
- try {
253
- const progress = progressReporter(extra);
254
- await progress.report(0, 2, 'Analyzing technologies');
255
- const output = await viewer.analyze(analysisRequest(input, workspaceId, context, progress.signal));
256
- const result = emptyServiceResult(workspaceId, {
257
- mode: input.mode,
258
- revision: output.graph.revision,
259
- graphHash: graphHash(output.graph),
260
- counts: counts(output.graph, output.artifacts.length),
261
- });
262
- result.code = output.graph.complete ? 'TECH_INSPECTED' : 'TECH_INSPECTED_PARTIAL';
263
- setInlineFilesScanned(result, output.graph.filesScanned);
264
- result.artifacts = output.artifacts.map(publicArtifactLink);
265
- result.validation = validation(output.graph);
266
- await progress.report(2, 2, 'Technology analysis complete');
267
- return toolResult(result);
268
- }
269
- catch (error) {
270
- return errorResult(error, workspaceId);
271
- }
272
- finally {
273
- releaseCaches();
274
- }
275
- });
276
- server.registerTool('hoi4.tech_render', {
99
+ inputSchema: inspectInputSchema,
100
+ outputSchema: analysisOutputSchema,
101
+ annotations: readOnlyTechnologyTool,
102
+ },
103
+ {
104
+ name: 'hoi4.tech_render',
277
105
  title: 'Render technology trees',
278
106
  description: 'Render source-linked JSON, SVG, PNG, and optional HTML technology views.',
279
- inputSchema: renderInput,
280
- outputSchema: renderOutput,
281
- annotations: readOnly,
282
- }, async (input, extra) => {
283
- const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
284
- const releaseCaches = cacheLifetime.begin();
285
- try {
286
- const progress = progressReporter(extra);
287
- const request = {
288
- workspaceId,
289
- view: input.view,
290
- ...(input.folderId === undefined ? {} : { folderId: input.folderId }),
291
- ...(input.technologyId === undefined ? {} : { technologyId: input.technologyId }),
292
- ...(input.categoryId === undefined ? {} : { categoryId: input.categoryId }),
293
- ...(input.targetId === undefined ? {} : { targetId: input.targetId }),
294
- ...(input.maxNodes === undefined ? {} : { maxNodes: input.maxNodes }),
295
- ...(input.includeHtml === undefined ? {} : { includeHtml: input.includeHtml }),
296
- ...(input.refresh === undefined ? {} : { refresh: input.refresh }),
297
- ...(context.principal === undefined ? {} : { principal: context.principal }),
298
- signal: progress.signal,
299
- };
300
- const output = await viewer.renderAndStore(request);
301
- const result = emptyServiceResult(workspaceId, {
302
- view: output.render.view,
303
- revision: output.graph.revision,
304
- graphHash: graphHash(output.graph),
305
- hashes: output.render.hashes,
306
- selectedNodes: output.render.selectedIds.length,
307
- omittedNodes: output.render.omittedNodeCount,
308
- focusedRenders: output.focused.length,
309
- sourceAccurate: output.render.sourceAccurate,
310
- });
311
- result.code = output.graph.complete ? 'TECH_RENDERED' : 'TECH_RENDERED_PARTIAL';
312
- setInlineFilesScanned(result, output.graph.filesScanned);
313
- result.artifacts = output.artifacts.map(publicArtifactLink);
314
- result.validation = validation(output.graph);
315
- return toolResult(result);
316
- }
317
- catch (error) {
318
- return errorResult(error, workspaceId);
319
- }
320
- finally {
321
- releaseCaches();
322
- }
323
- });
324
- server.registerTool('hoi4.tech_compare', {
107
+ inputSchema: renderInputSchema,
108
+ outputSchema: renderOutputSchema,
109
+ annotations: readOnlyTechnologyTool,
110
+ },
111
+ {
112
+ name: 'hoi4.tech_compare',
325
113
  title: 'Compare technology trees',
326
114
  description: 'Compare cached, resource-backed, current, or proposed source graphs without writes.',
327
- inputSchema: compareInput,
328
- outputSchema: compareOutput,
329
- annotations: readOnly,
330
- }, async (input, extra) => {
331
- const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
332
- const releaseCaches = cacheLifetime.begin();
333
- try {
334
- const progress = progressReporter(extra);
335
- const request = {
336
- workspaceId,
337
- ...(input.before === undefined
338
- ? {}
339
- : { before: input.before }),
340
- ...(input.after === undefined
341
- ? {}
342
- : { after: input.after }),
343
- ...(input.proposedSources === undefined
344
- ? {}
345
- : {
346
- proposedSources: input.proposedSources,
347
- }),
348
- ...(input.render === undefined ? {} : { render: input.render }),
349
- ...(input.maxRenderNodes === undefined ? {} : { maxRenderNodes: input.maxRenderNodes }),
350
- ...(input.refresh === undefined ? {} : { refresh: input.refresh }),
351
- ...(context.principal === undefined ? {} : { principal: context.principal }),
352
- signal: progress.signal,
353
- };
354
- const output = await viewer.compareAndStore(request);
355
- const comparison = output.comparison;
356
- const result = emptyServiceResult(workspaceId, {
357
- beforeRevision: comparison.beforeRevision,
358
- afterRevision: comparison.afterRevision,
359
- added: comparison.technologies.added.length,
360
- removed: comparison.technologies.removed.length,
361
- renamed: comparison.technologies.renamed.length,
362
- moved: comparison.technologies.moved.length,
363
- regressions: comparison.regressions.length,
364
- artifacts: output.artifacts.length,
365
- ...(output.render === undefined ? {} : { renderHashes: output.render.hashes }),
366
- });
367
- result.code =
368
- output.before.complete && output.after.complete
369
- ? 'TECH_COMPARED'
370
- : 'TECH_COMPARED_PARTIAL';
371
- setInlineFilesScanned(result, [...new Set([...output.before.filesScanned, ...output.after.filesScanned])].sort(compareCodeUnits));
372
- result.artifacts = output.artifacts.map(publicArtifactLink);
373
- result.validation = {
374
- passed: validation(output.before).passed && validation(output.after).passed,
375
- checks: [...validation(output.before).checks, ...validation(output.after).checks],
376
- };
377
- return toolResult(result);
378
- }
379
- catch (error) {
380
- return errorResult(error, workspaceId);
381
- }
382
- finally {
383
- releaseCaches();
384
- }
385
- });
115
+ inputSchema: compareInputSchema,
116
+ outputSchema: compareOutputSchema,
117
+ annotations: readOnlyTechnologyTool,
118
+ },
119
+ ];
120
+ export function registerTechnologyTools(server) {
121
+ registerLegacyTaskTools(server, technologyTaskTools);
386
122
  }
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto';
2
2
  import { isIP } from 'node:net';
3
3
  import { TextDecoder } from 'node:util';
4
4
  import express, {} from 'express';
5
+ import { toNodeHandler, toWebRequest } from '@modelcontextprotocol/node';
6
+ import { createMcpHandler, isLegacyRequest } from '@modelcontextprotocol/server';
5
7
  import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
6
8
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
9
  import { isInitializeRequest, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
@@ -9,6 +11,7 @@ import { HTTP_MAX_AGGREGATE_BODY_BYTES } from '../../core/configuration.js';
9
11
  import { canonicalJson } from '../../core/canonical.js';
10
12
  import { authenticate, bearerChallenge, HttpAuthError, protectedResourceMetadata, validateHttpDeployment, } from '../security/auth.js';
11
13
  import { BoundedEventStore, SharedEventStoreBudget } from './event-store.js';
14
+ import { createModernOperationServer } from '../server/create-modern-operations.js';
12
15
  import { FinalProtocolTransport } from './protocol-gate.js';
13
16
  const WRITE_SCOPED_TOOLS = new Set(['hoi4.focus_rewrite', 'hoi4.gui_rewrite', 'hoi4.map_rewrite']);
14
17
  const MCP_ALLOWED_METHODS = 'GET, POST, DELETE';
@@ -18,6 +21,8 @@ const SINGLETON_SECURITY_HEADERS = new Set([
18
21
  'content-type',
19
22
  'host',
20
23
  'last-event-id',
24
+ 'mcp-method',
25
+ 'mcp-name',
21
26
  'mcp-protocol-version',
22
27
  'mcp-session-id',
23
28
  'origin',
@@ -211,7 +216,8 @@ function rejectAmbiguousSecurityHeaders(request, response, next) {
211
216
  const counts = new Map();
212
217
  for (let index = 0; index < request.rawHeaders.length; index += 2) {
213
218
  const name = request.rawHeaders[index]?.toLowerCase();
214
- if (name !== undefined && SINGLETON_SECURITY_HEADERS.has(name)) {
219
+ if (name !== undefined &&
220
+ (SINGLETON_SECURITY_HEADERS.has(name) || name.startsWith('mcp-param-'))) {
215
221
  counts.set(name, (counts.get(name) ?? 0) + 1);
216
222
  }
217
223
  }
@@ -437,6 +443,51 @@ export async function startHttpServer(engine, configuration, serverFactory) {
437
443
  sendAuthError(response, configuration, authError);
438
444
  }
439
445
  };
446
+ const routeModernRequest = async (request, response) => {
447
+ // Preserve the legacy adapter's response contract for every claim-less request, including
448
+ // malformed JSON-RPC bodies. Explicit modern claims alone enter the SDK's strict ladder.
449
+ const body = request.body;
450
+ const params = isRecord(body) && isRecord(body.params) ? body.params : undefined;
451
+ const metadata = params !== undefined && isRecord(params._meta) ? params._meta : undefined;
452
+ const modernClaim = (isRecord(body) && body.method === 'server/discover') ||
453
+ request.headers['mcp-protocol-version'] === '2026-07-28' ||
454
+ request.headers['mcp-method'] !== undefined ||
455
+ request.headers['mcp-name'] !== undefined ||
456
+ Object.keys(request.headers).some((name) => name.startsWith('mcp-param-')) ||
457
+ (metadata !== undefined &&
458
+ Object.keys(metadata).some((name) => name.startsWith('io.modelcontextprotocol/')));
459
+ if (!modernClaim)
460
+ return false;
461
+ // An explicit malformed modern claim belongs to modern validation, never to a legacy session.
462
+ // The Node SDK helper concatenates Host and req.url. A validated absolute-form target
463
+ // must therefore be changed to origin-form only while this SDK adapter consumes it.
464
+ const originalUrl = request.url;
465
+ if (!originalUrl.startsWith('/')) {
466
+ const target = new URL(originalUrl);
467
+ request.url = `${target.pathname}${target.search}`;
468
+ }
469
+ try {
470
+ const probe = await toWebRequest(request, request.body);
471
+ if (await isLegacyRequest(probe, request.body))
472
+ return false;
473
+ const principal = request.authPrincipal;
474
+ if (principal === undefined) {
475
+ jsonRpcError(response, 401, 'Authentication context is missing');
476
+ return true;
477
+ }
478
+ // The only principal and scopes given to the modern core come from the authenticated
479
+ // middleware. No request envelope or caller-authored metadata can substitute for them.
480
+ const modern = createMcpHandler(() => createModernOperationServer(engine, {
481
+ principal: principal.principal,
482
+ scopes: [...principal.scopes],
483
+ }), { legacy: 'reject', responseMode: 'auto' });
484
+ await toNodeHandler(modern)(request, response, request.body);
485
+ return true;
486
+ }
487
+ finally {
488
+ request.url = originalUrl;
489
+ }
490
+ };
440
491
  app.all('/mcp', async (request, response, next) => {
441
492
  if (['GET', 'POST', 'DELETE'].includes(request.method)) {
442
493
  next();
@@ -575,6 +626,8 @@ export async function startHttpServer(engine, configuration, serverFactory) {
575
626
  };
576
627
  app.post('/mcp', authenticateMiddleware, async (request, response) => {
577
628
  try {
629
+ if (await routeModernRequest(request, response))
630
+ return;
578
631
  const sessionId = sessionHeader(request);
579
632
  if (rejectInvalidProtocolHeader(request, response, sessionId !== undefined))
580
633
  return;
@@ -696,6 +749,8 @@ export async function startHttpServer(engine, configuration, serverFactory) {
696
749
  }
697
750
  });
698
751
  app.get('/mcp', authenticateMiddleware, async (request, response) => {
752
+ if (await routeModernRequest(request, response))
753
+ return;
699
754
  if (rejectInvalidProtocolHeader(request, response, true))
700
755
  return;
701
756
  const session = await findSession(request, response);
@@ -740,6 +795,8 @@ export async function startHttpServer(engine, configuration, serverFactory) {
740
795
  await session.transport.handleRequest(request, response);
741
796
  });
742
797
  app.delete('/mcp', authenticateMiddleware, async (request, response) => {
798
+ if (await routeModernRequest(request, response))
799
+ return;
743
800
  if (rejectInvalidProtocolHeader(request, response, true))
744
801
  return;
745
802
  const session = await findSession(request, response);
@@ -0,0 +1,109 @@
1
+ import { isJSONRPCRequest, isJSONRPCNotification, PROTOCOL_VERSION_META_KEY, Server, } from '@modelcontextprotocol/server';
2
+ import { CancelledNotificationSchema } from '@modelcontextprotocol/core';
3
+ import { secureId } from '../../core/canonical.js';
4
+ export const MODERN_TASK_ROUTES = new Map([
5
+ ['tasks/get', 'io.github.klimPaskov/hoi4-agent-tools.internal/tasks/get'],
6
+ ['tasks/update', 'io.github.klimPaskov/hoi4-agent-tools.internal/tasks/update'],
7
+ ['tasks/cancel', 'io.github.klimPaskov/hoi4-agent-tools.internal/tasks/cancel'],
8
+ ]);
9
+ const privateMethods = new Set(MODERN_TASK_ROUTES.values());
10
+ /**
11
+ * SDK 2.0.0 issue #2598: historical tasks/get and tasks/cancel are rejected by
12
+ * its core-method era gate before registered extension handlers can run.
13
+ * Rewrite only these extension method names at the public Transport seam.
14
+ * The SDK still validates the envelope and pins the era; HTTP/auth metadata,
15
+ * request IDs, cancellation, params, and every other method remain untouched.
16
+ */
17
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- Public advanced Server API is needed to decorate its transport; McpServer cannot inject one.
18
+ export class ModernTaskRoutingServer extends Server {
19
+ async connect(transport) {
20
+ // SDK 2.0.0's cancellation handler ignores falsy IDs (0 and ''). Give only those
21
+ // active requests private truthy IDs internally and preserve their exact wire IDs.
22
+ const requestAliases = new Map();
23
+ const responseAliases = new Map();
24
+ const releaseAlias = (alias) => {
25
+ const original = responseAliases.get(alias);
26
+ if (original !== undefined)
27
+ requestAliases.delete(original);
28
+ responseAliases.delete(alias);
29
+ };
30
+ const routed = {
31
+ get sessionId() {
32
+ return transport.sessionId;
33
+ },
34
+ ...(transport.hasPerRequestStream === undefined
35
+ ? {}
36
+ : { hasPerRequestStream: transport.hasPerRequestStream }),
37
+ start: async () => {
38
+ transport.onclose = () => {
39
+ requestAliases.clear();
40
+ responseAliases.clear();
41
+ routed.onclose?.();
42
+ };
43
+ transport.onerror = (error) => routed.onerror?.(error);
44
+ transport.onmessage = (message, extra) => {
45
+ if (isJSONRPCRequest(message) && privateMethods.has(message.method)) {
46
+ void transport
47
+ .send({
48
+ jsonrpc: '2.0',
49
+ id: message.id,
50
+ error: { code: -32601, message: 'Method not found' },
51
+ })
52
+ .catch((error) => routed.onerror?.(error instanceof Error ? error : new Error('Response could not be sent')));
53
+ return;
54
+ }
55
+ const alias = isJSONRPCRequest(message) &&
56
+ message.params?._meta?.[PROTOCOL_VERSION_META_KEY] === '2026-07-28'
57
+ ? MODERN_TASK_ROUTES.get(message.method)
58
+ : undefined;
59
+ let inbound = alias === undefined ? message : { ...message, method: alias };
60
+ if (isJSONRPCRequest(inbound) && (inbound.id === 0 || inbound.id === '')) {
61
+ if (requestAliases.has(inbound.id)) {
62
+ void transport
63
+ .send({
64
+ jsonrpc: '2.0',
65
+ id: inbound.id,
66
+ error: { code: -32600, message: 'Request ID is already active' },
67
+ })
68
+ .catch((error) => routed.onerror?.(error instanceof Error ? error : new Error('Response could not be sent')));
69
+ return;
70
+ }
71
+ const requestAlias = secureId('mcp_request');
72
+ requestAliases.set(inbound.id, requestAlias);
73
+ responseAliases.set(requestAlias, inbound.id);
74
+ inbound = { ...inbound, id: requestAlias };
75
+ }
76
+ else if (isJSONRPCNotification(inbound) &&
77
+ inbound.method === 'notifications/cancelled' &&
78
+ CancelledNotificationSchema.safeParse(inbound).success) {
79
+ const original = inbound.params?.requestId;
80
+ const requestAlias = original === 0 || original === '' ? requestAliases.get(original) : undefined;
81
+ if (requestAlias !== undefined) {
82
+ inbound = { ...inbound, params: { ...inbound.params, requestId: requestAlias } };
83
+ releaseAlias(requestAlias);
84
+ }
85
+ }
86
+ routed.onmessage?.(inbound, extra);
87
+ };
88
+ await transport.start();
89
+ },
90
+ send: (message, options) => {
91
+ const original = 'id' in message && typeof message.id === 'string'
92
+ ? responseAliases.get(message.id)
93
+ : undefined;
94
+ const related = typeof options?.relatedRequestId === 'string'
95
+ ? responseAliases.get(options.relatedRequestId)
96
+ : undefined;
97
+ const outbound = original === undefined ? message : { ...message, id: original };
98
+ if (original !== undefined && 'id' in message && typeof message.id === 'string')
99
+ releaseAlias(message.id);
100
+ return transport.send(outbound, related === undefined ? options : { ...options, relatedRequestId: related });
101
+ },
102
+ close: () => transport.close(),
103
+ setProtocolVersion: (version) => transport.setProtocolVersion?.(version),
104
+ setSupportedProtocolVersions: (versions) => transport.setSupportedProtocolVersions?.(versions),
105
+ };
106
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- Connect the public advanced Server API after wrapping the transport.
107
+ await super.connect(routed);
108
+ }
109
+ }