@battlegrid/mcp-server 31.0.3 → 31.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,16 +24,205 @@ Seeing package `31.x` alongside handshake `battlegrid@33.x` — the package **be
24
24
 
25
25
  **What this changes for you:** nothing about how you call anything. Upgrading the package no longer waits on a server deploy, and a server deploy no longer strands you on a package that names the wrong contract — reconnect and the announcement follows. **Contract breaking-change notes are no longer keyed to package versions**, since a contract move is no longer a release here; the v11-and-earlier notes below are kept as history, and the live vocabulary is always discovery.
26
26
 
27
- ## Contract history — v12 → v33
27
+ ## Contract history — v37 → v48.1
28
+
29
+ Eleven majors reached authors while this section stopped at v36. That gap is the mechanism, not an
30
+ oversight: since v31 a contract move needs no release here, so nothing forced a note to be written —
31
+ and the documentation ships inside the tarball, so a note written but unpublished reaches nobody.
32
+ Both halves are now closed by a rule keyed to the *served* contract rather than to a release of this
33
+ package.
34
+
35
+ ### Changed meaning, unchanged shape
36
+
37
+ - **Three tools serve different values for identical input** (47.3.0, `derive-scan-fetch-from-report`).
38
+ The radar scan leg now derives its timeframe fetch from the strategy's **report** rather than the
39
+ on-duty agent's three perception rungs, so a required condition addressing an absolute timeframe
40
+ outside those rungs — never evaluated at scan before — now is. No field moves; the values do:
41
+
42
+ | Tool | What moves |
43
+ |---|---|
44
+ | `preview_radar_resolution` | `conditionReach[].reachReason` goes `AGENT_TIMEFRAME` → `null` |
45
+ | `get_radar_activity` | `scanReachReason` moves the same way, **on new rows only** — rows already written keep what they were recorded with |
46
+ | `get_agent_coin_qualification` | `reachReason` moves the same way; its sibling `verdict` moves `UNMEASURABLE` → a decided verdict |
47
+
48
+ **Read the last one carefully:** a client treating `UNMEASURABLE` as "this gate is switched off"
49
+ will now see that gate **BLOCK**. `AGENT_TIMEFRAME` keeps its member and narrows to the one cause
50
+ no fetch can discharge. Nothing in the payload tells you this moved.
51
+
52
+ ### Rejected input — something you author is no longer accepted
53
+
54
+ - **A benchmark-bound section no longer accepts crowd metrics or rank transforms** (49.0.0,
55
+ `fix-benchmark-legality-save-path`). On a custom section carrying a non-null `benchmarkTicker`, a
56
+ column whose metric is enrichment-stage (the `CROWD_*` family, `FLOW_ALIGN`, `SMART_RETAIL`,
57
+ `CAPTAIN_CONF`, `CONFIDENCE`, `SETTLED_AT`, the `PERP_SPOT_*` trio) is refused with
58
+ `REPORT_COLUMN_BENCHMARK_METRIC_UNSUPPORTED`, and one carrying a `rank` transform in either the
59
+ direct or the chained position with `REPORT_COLUMN_BENCHMARK_TRANSFORM_UNSUPPORTED`. Every tool
60
+ accepting a section array is affected, and no other byte of your body changes.
61
+
62
+ **Fix it by moving the column, not by retrying.** Both readings are defined *relative to the
63
+ cohort being evaluated* — a crowd reading is what this session's players did, a rank is a position
64
+ among the coins under evaluation — and a benchmark is deliberately outside that cohort. Neither
65
+ has a value there, which is why such a column could never render. Put it on an ordinary section
66
+ (`benchmarkTicker: null`), or drop it.
67
+
68
+ **This is a fix, not a new rule.** The restriction shipped with benchmark sections and was already
69
+ enforced by the column builder, by report materialization, and by `get_strategy_column_contract` —
70
+ so an author who checked a column against discovery first has been seeing this refusal all along.
71
+ What changed is that the SAVE path now asks the same question. Previously it did not, so such a
72
+ section persisted and then failed at every evaluation instead of at authoring.
73
+
74
+ - **A custom report section no longer accepts `timeframe`** (48.0.0,
75
+ `remove-section-anchor-override`). The per-section anchor override is gone. Every tool accepting a
76
+ section array — `compile_strategy_plan`, `preview_strategy_report`, `derive_strategy_rule_view`,
77
+ and `apply_strategy_plan` — refuses a section carrying it: `sections[N]: Unrecognized key(s)`,
78
+ with no other byte of your body changing. Drop the key. A section's columns resolve against the
79
+ **strategy** timeframe, and a column reaches any other timeframe by pinning it on the column
80
+ (`timeframe: { abs: '4h' }`) — which it could always do. Relative column references
81
+ (`anchor`/`lower`/`regime`) are untouched, and are the point: they track the strategy.
82
+
83
+ - **A custom report section requires `notes`** (43.0.0, `add-authored-section-notes`). Every tool
84
+ accepting a section array — `compile_strategy_plan`, `preview_strategy_report`,
85
+ `derive_strategy_rule_view` — refuses a section without it: `sections[N].notes: Required`, with no
86
+ byte of your body changing. Send explicit `null` for "no note". It is required rather than optional
87
+ because these payloads are a FULL REPLACE: an omitted key and an explicit `null` would be the same
88
+ request on the wire, so every rebuild site would silently clear a note its author wrote.
89
+ `benchmarkTicker` carries the same required-nullable discipline for the same reason.
90
+
91
+ - **Every condition requires `clock` and `closes`** (44.0.0, `add-condition-clock`), and **`exit`**
92
+ arrives with them. A condition entry now carries eight keys, not five. `clock` is `"LIVE"` (the
93
+ previous behaviour — the forming bar) or `"CLOSE"` (settled bars); `closes` is how many consecutive
94
+ closed bars must read true, 1–5, and is always `1` under LIVE. No wire default, for the same
95
+ whole-set-replacement reason as `notes`: a defaulted key would let an unrelated re-save silently
96
+ un-clock an enforced money gate back to forming-bar evidence.
97
+
98
+ A `CLOSE` clock is accepted only where a closed frame can change the reading — the clause must
99
+ resolve from the coin's own candle series at offset 0. Frame-inert operands (perp-payload scalars,
100
+ published rolling changes, ranks, zone entities, regime labels, enrichment metrics, session
101
+ scalars) are refused with `CONDITION_CLOCK_OPERAND_ILLEGAL` naming the header and the remedy: split
102
+ that clause into its own LIVE condition and `conditionRef` it. `exit: true` is legal only under
103
+ `clock: "CLOSE"` — an exit fired on a forming bar is an intrabar exit.
104
+
105
+ - **A strategy requires a seven-key `entry` object** (44.0.0 for four keys, 47.0.0 for three more —
106
+ `add-entry-on-close`, `add-level-trigger-execution`). Required on every CREATE, on
107
+ `compile_strategy_plan`, `apply_strategy_plan` and `update_intelligence_agent`. A client sending
108
+ 44.0.0's four-key object is refused under 47 with `entry.levelSource: Required` without one byte of
109
+ it changing.
110
+
111
+ ```jsonc
112
+ "entry": {
113
+ "trigger": "AT_SIGNAL", // | ON_CANDLE_CLOSE | STOP_THROUGH_LEVEL | ON_RETEST
114
+ "confirmTf": "4h", // the strategy timeframe or the rung below it — nothing else
115
+ "closes": 1, // 1–5; must be 1 unless ON_CANDLE_CLOSE
116
+ "bandAtrMultiple": 1.0, // > 0, and <= the platform's entry-deviation gate
117
+ "levelSource": "SWING_HIGH", // | SWING_LOW | BOLLINGER_UPPER | BOLLINGER_LOWER
118
+ "levelOffsetAtrMultiple": 0, // 0–2, UNSIGNED; must be 0 unless a level trigger
119
+ "validForBars": 4 // 1–24 of the strategy's own bars
120
+ }
121
+ ```
122
+
123
+ `AT_SIGNAL` with those values is byte-identical to pre-44 behaviour. The legality matrix runs one
124
+ way: all seven keys are always present, so a MEANINGFUL value under a trigger that ignores it is
125
+ refused rather than accepted-and-dropped — a dial never silently does nothing.
126
+
127
+ - **A `strategyTimeframe` the platform does not ingest is refused** (39.0.0,
128
+ `move-renderer-to-rendered-section`) on `get_coin_market_context`, where it was previously
129
+ accepted. The number is the only signal a client gets.
130
+
131
+ - **`eventType` gains `ENTRY_EXPIRED_UNCONFIRMED`** (48.1.0, `fix-entry-arming-lifecycle`) on
132
+ `get_radar_activity` and `get_radar_activity_summary` — an armed entry that reached its episode
133
+ lifetime without ever receiving a confirming close. A client holding its own closed copy of that
134
+ enum rejects the new member; one that switches exhaustively on it needs the branch.
135
+
136
+ `entryVoidCause` is deliberately **unchanged** and still carries exactly `BAND`, `CONDITIONS`,
137
+ `STRUCTURAL`. Each of those is a measurement taken *at* a close, so an episode that reached no
138
+ close gets its own event type rather than a fourth cause — `ENTRY_VOIDED` means "called off at the
139
+ close", a claim this outcome must not make.
140
+
141
+ The authoring boundary also gains a refusal with **no schema change**: a strategy declaring an
142
+ arming trigger whose `required` conditions read the `LIVE` clock is rejected on the existing
143
+ `VALIDATION_ERROR` code, naming the offending condition key. Same code, same shape, new reason —
144
+ so nothing in the published schema tells you it can now happen.
145
+
146
+ ### Removed — no alias exists
147
+
148
+ - **`get_coin_market_context` is REMOVED** (40.0.0, `retire-get-coin-market-context`). Calling it
149
+ returns an unknown-tool error. There is deliberately **no alias**: a silent redirect would hide a
150
+ payload shape change from a client that never asked for one. Use `get_market_context`.
151
+
152
+ - **`get_macd_heatmap` leaves the published surface** (41.0.0). Same shape of break, same absence of
153
+ an alias.
154
+
155
+ - **`isPrimary` is removed from every published `EvaluatedSignal`** (38.0.0) — `get_signal_log`,
156
+ `get_public_agent_signal_log_detail`, and every other tool returning a signal scorecard. Reading it
157
+ now finds the key absent rather than false.
158
+
159
+ ### Reshaped output — the same call returns a different shape
160
+
161
+ - **The normalized report section loses `timeframe`** (48.0.0, `remove-section-anchor-override`), on
162
+ every tool that publishes a strategy: `get_strategy`, `fork_strategy`, `archive_strategy`,
163
+ `restore_strategy`, `update_strategy_signal_rule`, `apply_strategy_plan`, and
164
+ `compile_strategy_plan`'s post-state. A strict parser rejects the shorter object; a lenient one
165
+ reads a section whose anchor is the strategy timeframe, which it now always is.
166
+
167
+ - **`get_strategy_column_contract` renames its anchor, both ways** (48.0.0). The request field
168
+ `sectionTimeframe` becomes `anchorTimeframe` — same meaning, and still optional. On the response,
169
+ `timeframe.requiresSectionTimeframe` becomes `requiresAnchorTimeframe`, and
170
+ `timeframe.sectionTimeframeOverrideAllowed` is **removed**: it published whether a column could go
171
+ in an anchor-overridden section, and no section can be overridden. One call also stops being
172
+ refused — a timeframe-inert metric supplied with an anchor now compiles.
173
+ `REPORT_COLUMN_SECTION_TIMEFRAME_UNSUPPORTED` leaves the `authoringCode` vocabulary.
174
+
175
+ - **`RenderedSection.notes` stops carrying provenance** (42.0.0, `separate-section-facts-from-read`),
176
+ on `preview_strategy_report`, `compile_strategy_plan` and `get_market_context`. A new REQUIRED
177
+ `provenance: string[]` carries it instead. A client reading provenance out of `notes` now reads an
178
+ author's prose, or nothing — which is a silent misread, not an error.
179
+
180
+ - **`preview_strategy_report.renderedSections[]` gains `authoredNote`** (43.0.0), the author's read
181
+ for a custom row and `null` on a platform row. Additive, but published on a `.strict()` shape.
182
+
183
+ - **`get_radar_activity` serves its evaluation curve on the FIRST PAGE ONLY** (37.0.0), and
184
+ `get_radar_activity_summary` is added. A client reading the curve off a later page finds it absent.
185
+
186
+ ### Widened enum — new members your own copy rejects
187
+
188
+ - **The authorable metric vocabulary widens by 29 keys** (46.1.0, `add-indicator-catalog-coverage`):
189
+ Keltner (`KC_UPPER`/`KC_MID`/`KC_LOWER`), Supertrend (`ST_LINE`/`ST_DIR`), Hull (`HMA20`),
190
+ WaveTrend (`WT1`/`WT2`), QQE (`QQE_RSI_MA`/`QQE_STOP`), Parabolic SAR (`PSAR`), Ichimoku
191
+ (`ICHI_CONV`/`ICHI_BASE`/`ICHI_SPAN_A`/`ICHI_SPAN_B`/`ICHI_LAG`), Williams %R (`WILLR14`),
192
+ Stochastic RSI (`STOCH_RSI14`), session pivots (`PIVOT_P`/`PIVOT_R1`–`R3`/`PIVOT_S1`–`S3`), plus
193
+ four already-published fields that became addressable: `BB_UPPER`, `BB_LOWER`, `DI_PLUS`,
194
+ `DI_MINUS`.
195
+
196
+ Additive on the wire — every request you can send today is still accepted. It is called out here
197
+ because a client holding its own closed copy of the metric enum rejects the new members, and
198
+ because **an agent holding a cached belief that these are inexpressible will substitute for a
199
+ primitive the platform now serves**. That failure raises no error at all: the author is simply told
200
+ a strategy cannot be built.
201
+
202
+ - **`EntryTrigger` gains `STOP_THROUGH_LEVEL` and `ON_RETEST`, and `EntryLevelSource` is published
203
+ for the first time** with four members (47.0.0). Additive on their own; the required keys above are
204
+ what make that bump a major.
205
+
206
+ - **Each signal-checklist item's `measured` object gains a required `triggered` boolean** (47.2.0,
207
+ `fix-entry-prompt-signal-evidence`) on `get_signal_log` and `get_public_agent_signal_log_detail`,
208
+ on the `numeric` and `categorical` arms. Additive and MINOR — a client ignoring it is unaffected —
209
+ but it is named here because a client parsing that output strictly rejects the new key, and because
210
+ leaving 47.2 unlisted would make a reader wonder what happened to it.
211
+
212
+ The `unavailable` arm deliberately does **not** carry `triggered`: "the claim could not be joined to
213
+ a stored result" and "the signal did not fire" are different states, and leaving the key off that
214
+ arm makes conflating them a type error rather than a convention.
215
+
216
+ ## Contract history — v12 → v36
28
217
 
29
218
  > **The number in this heading is a CONTRACT version, not this package's version.** The npm badge at the top
30
219
  > tracks the proxy's own code; this section tracks the server's wire contract. They move independently **by
31
220
  > design**: a contract move needs no release here, because a connected proxy relays the contract out of the
32
- > upstream handshake rather than declaring it. So a package on `31.x` listing contract history up to `33.x` is
221
+ > upstream handshake rather than declaring it. So a package on `31.x` listing contract history up to `36.x` is
33
222
  > correct — not a version someone forgot to bump. Read the live pair from the startup stderr lines or
34
223
  > `GET /mcp/version`; see [Rediscovery & versioning](#rediscovery--versioning) for why.
35
224
 
36
- These are the server contract breaks between contract 12 and contract 33. Most of the span shipped while the package sat at `11.0.0`; contract 31 landed after this package reached `31.0.0`, and the two numbers matching is coincidence — since v31 the announced contract is relayed from the server, so a package version says nothing about a contract version. They are **contract** history, not package releases: from v31 the announced contract is relayed live and a contract move is no longer a release here. Grouped by what a client observes, with the contract version that introduced each.
225
+ These are the server contract breaks between contract 12 and contract 36. Most of the span shipped while the package sat at `11.0.0`; contract 31 landed after this package reached `31.0.0`, and the two numbers matching is coincidence — since v31 the announced contract is relayed from the server, so a package version says nothing about a contract version. They are **contract** history, not package releases: from v31 the announced contract is relayed live and a contract move is no longer a release here. Grouped by what a client observes, with the contract version that introduced each.
37
226
 
38
227
  **The proxy itself is unchanged.** It embeds no schemas, pins no contract version, and forwards `{ request }` verbatim. Every break below lands on whatever *authors* the payload or *reads* the result, never on the proxy.
39
228
 
@@ -45,6 +234,14 @@ These are the server contract breaks between contract 12 and contract 33. Most o
45
234
 
46
235
  ### Rejected input — something you author is no longer accepted
47
236
 
237
+ - **`upsert_deployment_policy` requires `enabled`** (35.0.0, `add-arena-deployment-pause`). The arena deployment gains an owner-owned pause, and the flag that carries it is **required, not optional** — a body accepted under contract 34 is refused under 35 without one byte of it changing. Required is the whole point: this call replaces the entire policy, so an omitted key and an explicit `true` would be the same request on the wire, and every client that rebuilt a policy without the flag would silently resume a deployment its owner had paused. Read the value from `get_deployment_policy` and send it back. **There is no separate pause verb** — pausing and resuming are this same call with the flag flipped.
238
+
239
+ Do not reach for `enabled: false` to un-deploy. It keeps every slot and stops play, which is the opposite of withdrawing: `delete_deployment_policy` is the withdrawal verb, and it discards the rules permanently. `upsert_deployment_policy` refuses an empty slot set, and its rejection names both routes.
240
+
241
+ - **A signal rule flagged `required` at allocation Off is rejected** (34.0.0, `enforce-required-allocation-invariant`). `required` and `allocation` are two independently editable fields encoding one thing — whether and how a signal participates — so `{ required: true, allocation: 0 }` was representable and meant nothing: the scorecard's triggered set already excludes Off, so such a rule could neither satisfy `minRequiredCount` nor block a trade. It is now refused on **every** rule-writing surface — `compile_strategy_plan`, `apply_strategy_plan` and `update_strategy_signal_rule` alike.
242
+
243
+ Three things make this one easy to trip over. It is an **input-acceptance narrowing**: a payload accepted under contract 33 is refused under 34 without one byte of it changing. It is **invisible in the published schema** — `zod-to-json-schema` drops effects by construction — so you cannot pre-validate it from `tools/list`, and the typed error IS the contract: read `details.inertRequiredSignalIds`, which carries the complete sorted list of offending signals, rather than the message, which names a bounded prefix. And **nothing is repaired for you**: the server will not raise the allocation (that would invent a scoring weight you never chose) nor clear the flag (that would discard your intent silently). Pick one and resend; either satisfies the boundary.
244
+
48
245
  - **`apply_strategy_plan` no longer accepts the plan** (33.0.0, `rehydrate-approved-plan-on-apply`). Its input narrows to `{ planToken, confirm }`. A `plan` member is **rejected as an unknown key** — not accepted, not ignored, and with no transitional dual shape — so every client that built the payload breaks on the next connection, which before this is what every published surface told it to do. The server keeps the plan its own compile approved and reads it back, so **you copy nothing out of the compile response**: forward `planToken` byte-for-byte and confirm.
49
246
 
50
247
  Three consequences worth knowing. The 256,000-byte cap on the apply payload is **gone with the payload**, so a large authored surface no longer becomes impossible to apply through a conversational client; the compiled plan is still capped and compile still enforces it. `PLAN_APPROVAL_NOT_FOUND` joins the error vocabulary for a token no approved plan answers to — already applied, lapsed, or never issued, all one code, because the recovery is the same in each case: compile again. And a validation refusal (a quota, a name collision, a bound agent that changed, a moved catalog) now **leaves the approved plan applicable** — clear the cause and confirm again with the same token while it lives, rather than recompiling.
@@ -94,6 +291,18 @@ These are the server contract breaks between contract 12 and contract 33. Most o
94
291
 
95
292
  ### Moved or reshaped output — a field you read is somewhere else
96
293
 
294
+ - **The signal scorecard stops serializing its entries three times over** (36.0.0, `mcp-signal-log-contents`). `get_signal_log` and `get_public_agent_signal_log_detail` drop `scorecard.triggeredSignals`, `scorecard.primarySignals` and `scorecard.supportingSignals`. Every one held the **same entry objects** `allEvaluatedSignals` already carried, so each is one filter over flags every entry still publishes:
295
+
296
+ | Removed | Read instead |
297
+ |---|---|
298
+ | `triggeredSignals` | `allEvaluatedSignals.filter(s => s.triggered)` |
299
+ | `primarySignals` | `allEvaluatedSignals.filter(s => s.triggered && s.isPrimary)` |
300
+ | `supportingSignals` | `allEvaluatedSignals.filter(s => s.triggered && !s.isPrimary)` |
301
+
302
+ **Keep the `triggered` half of those last two predicates.** Both collections were triggered-only by construction, so filtering on `isPrimary` alone surfaces signals that never fired — a silent widening, not an error. No field is removed from an entry: the key set on an `allEvaluatedSignals` member is unchanged, every evaluated signal is still returned whether or not it triggered, and `details` prose and `indicatorValues` are intact. There is no opt-in to get the three back and no default filter. Breaking only if you read one of the three names; on an 84-signal / 16-triggered log the duplication was 10,682 bytes, 28% of the scorecard, carrying no information.
303
+
304
+ - **The fleet roll-up on `list_deployment_policies` drops `unconfigured` and gains `paused`** (35.0.0, `add-arena-deployment-pause`, `fix-arena-deployment-undeploy`). `unconfigured` counted a deployment holding zero slots — a state that can no longer exist, because a stored policy now carries at least one slot and the withdrawn state is the **absence** of a policy rather than an empty one. The bucket was constant `0` at the moment of removal, so no number you read was wrong; a client reading the key still breaks on it, which is why this is a break and not a cleanup. `paused` is the owner's own switch, counted separately from `retired` — an administrator disabling the arena — because conflating them tells an owner to wait for something that will not happen. Every policy lands in exactly one bucket, so the buckets sum to `arenas`: worth asserting if you reconcile these counts.
305
+
97
306
  - **`AdminApprovedModelDTO.isActive` became `lifecycle`** (32.0.0, `remove-agent-presets`) — `AVAILABLE` / `DEPRECATED` / `RETIRED`. The boolean conflated "offered in the picker" with "bound agents may run", so there was no way to stop offering a model without hard-blocking every agent already on it. A client switching on `isActive` must switch on `lifecycle`, and **must not treat `DEPRECATED` as blocked**: that is the state which keeps bound agents running. The agent read DTO drops `brainPreset` in the same move — the marker recorded which named bundle an owner clicked, never a value the runtime read, and the model and soul it stamped are unchanged on every agent.
98
307
 
99
308
  - **`approvedPlan` is one object, not an operation union** (31.0.0, resolving #4495). It was published as a discriminated union whose discriminator does not survive JSON-Schema conversion, so what actually shipped was a bare `anyOf`: validating a failing compile response gave you every arm's errors with empty instance paths, and the top one typically complained that an UPDATE was missing `creationSeed` — a CREATE-only key — while the field that really failed went unnamed. It is now one object with a literal `operation` discriminator, and `creationSeed`, `expectedRevision` and `bindingImpact` are **required and nullable on every operation**: a CREATE plan carries a seed and `expectedRevision: null`, an UPDATE/RESTORE plan the reverse. **If you narrowed on the union arms, read `operation` instead and expect explicit `null`s rather than absent keys.** If you read those fields without narrowing, nothing changes except that they may now be null.
@@ -142,6 +351,8 @@ These are the server contract breaks between contract 12 and contract 33. Most o
142
351
 
143
352
  ### Widened enum — new members your own copy rejects
144
353
 
354
+ - **`DeploymentResolutionStatus` gains `PAUSED`** (35.0.0, `add-arena-deployment-pause`), returned by `get_deployment_policy`, `list_deployment_policies` and `preview_deployment_resolution`. A client switching exhaustively on the status must add the branch. Two properties are not obvious from the name: it is answered **before any slot is resolved**, so a paused deployment discloses no agent identity and carries `regimeUsed: null`; and its `targetSession` is nullable, because a deployment can be paused on an arena with no upcoming session and the pause is still the true answer. Do not re-derive the pause from the `enabled` flag beside it — the served status is the answer on every path, and those two disagreeing is the defect this closed.
355
+
145
356
  - **Seven plan-token failures became their own error codes** (31.0.0, resolving #4495). `TOKEN_EXPIRED`, `TOKEN_BINDING_MISMATCH`, `INVALID_TOKEN_SIGNATURE`, `INVALID_TOKEN_FORMAT`, `INVALID_TOKEN_CLAIMS`, `INVALID_DIGEST_MATERIAL` and `INVALID_MATERIALIZATION_FENCE` all used to arrive as a bare `INTERNAL_ERROR` — the server wrote the true reason to its own audit log and discarded it at the boundary, so a refused apply told you nothing. They now arrive as themselves, over MCP and HTTP alike, with 409-class status for the two state-conflict codes and 400-class for the five malformed-material codes. A client switching exhaustively on error codes must add the branches; one rendering unknown codes generically is unaffected. Two are worth handling by name: `TOKEN_EXPIRED` means recompile (the token lives five minutes), and `INVALID_TOKEN_SIGNATURE` usually means the token was not forwarded verbatim — it is opaque, so copy it byte-for-byte and never retype or reconstruct it.
146
357
 
147
358
  - `TradeEvaluationAttemptReasonCode` gains `OPEN_POSITION_CHECK_UNAVAILABLE` (19.4.0), splitting a code that previously reported a platform fault as a fact about your account.
@@ -154,6 +365,38 @@ Nothing in the proxy changes. No configuration, no environment variable, no call
154
365
 
155
366
  ### Additive in the same span
156
367
 
368
+ - **`get_regime_snapshot` publishes the evidence behind the verdict** (47.1.0,
369
+ `publish-regime-classification-evidence`). The snapshot gains `evidence`: the quantities the
370
+ classifier read, the gates it tested them against, the signed margin to the gate deciding whether
371
+ the current label survives, and the two decision facts only the classifier holds — `gateState`
372
+ (`cleared` / `held` / `dropped`) and `directionSource` (`di` / `ema`). Nothing narrows; a client
373
+ that ignores the field is unaffected.
374
+
375
+ Read `gateState` before you trust a trend label: **`held` means the ADX hysteresis buffer is
376
+ carrying the PREVIOUS bar's label rather than this bar re-confirming it** — a materially weaker
377
+ claim wearing the same word, and one no client could previously detect. `directionSource: 'ema'`
378
+ is the same shape of warning: the direction came from the fallback that fires precisely when the
379
+ DI spread is indecisive. The margin is signed so **positive always means "the current label
380
+ survives by this much"**, in every gate state, so it is safe to branch on its sign.
381
+
382
+ `conviction` is a BRANCH DISCRIMINATOR, not a confidence: it encodes *which* rule in the priority
383
+ ladder matched, not how comfortably it matched. The margins carry comfort. A client reading
384
+ conviction as a strength score is reading it wrong, and always was — this release just makes the
385
+ alternative available.
386
+
387
+ - **Thirteen metric keys join the catalog** (47.1.0) — the `regime` family gains `REGIME_STATE`,
388
+ `REGIME_CONVICTION`, `REGIME_RUN_BARS`, `REGIME_TREND_GATE`, `REGIME_TREND_MARGIN`,
389
+ `REGIME_TREND_SOURCE`, `REGIME_DI_SPREAD`, `REGIME_VOL_ATR_RATIO`, `REGIME_VOL_BBW_RATIO`,
390
+ `REGIME_MOM_BULL_VOTES`, `REGIME_MOM_BEAR_VOTES`, `REGIME_CRASH_MARGIN` and `REGIME_CRASH_LATCH`,
391
+ making the composite regime and its evidence addressable in a report column or condition for the
392
+ first time. Only a client that switches exhaustively on `MetricKey` needs new branches.
393
+
394
+ Not a contract change, but worth knowing if you author conditions: the report grammar's regime
395
+ metrics now resolve from the **confirmed close** on every path. They previously resolved from the
396
+ forming bar when a report was rendered and the confirmed close when the scan swept, so the same
397
+ condition could read differently in preview than in production. Same wire shape; same bar
398
+ everywhere now.
399
+
157
400
  `27.1.0` exit-policy authoring input on `compile_strategy_plan` · `19.2.0` `get_account_state` account identity · `19.1.0` Standing Orders marker authoring · `18.4.0` `list_gate_blocks` summary groups · `18.3.0` radar maintenance pause · `18.1.0` protection geometry · `17.2.0` break-even/trailing status · `17.1.0` `get_signal_log` condition evaluation · `13.1.0` four owner-scoped read tools · `12.1.0` cross-venue spot price metrics · `11.1.0` discoverable rate limit.
158
401
 
159
402
  ## v11 and earlier — contract history (v6 → v11)
@@ -517,7 +760,6 @@ Tools, prompts, and resources are **discovered live** from the connected server
517
760
  | 24.0.0 | Server contract v24.0.0, **breaking**: the post-entry exit policy moves from the agent to the strategy. `create_agent`/`update_agent` stop accepting `tradingConfig.positionManagement` on the shared `.strict()` `TradingConfigSchema`, so a client still sending it is rejected rather than ignored — the same input-acceptance narrowing that made v4, v5, v6, v8, v10 and v23 majors. On the read side `AgentTradingConfigDTO` drops the nested block on every agent-returning tool and the explorer trading spec drops it too; `get_trading_config_catalog` drops `positionManagementPresets` and the `defaultPositionMgmt*` trading defaults. The pistol-preset ladder (COLT / WEBLEY / BERETTA / LUGER / WALTHER) is **retired, not renamed** — once the values live on the strategy, the strategy is the named bundle. Additive on the authoring surface in the same bump: `compile_strategy_plan`/`apply_strategy_plan` post-state gains the twelve authored keys beside the trade-level trio, and the plan diff gains a `positionManagement` axis. Behaviourally the umbrella `enabled` flag is **deleted** rather than moved: each mechanism toggle is the whole truth for that mechanism, so a client can no longer express "trailing on, management off". Never published as a package version |
518
761
  | 26.0.0 | Server contract v26.0.0, **breaking**: both entry-lifecycle guards stop being agent configuration. `create_agent`/`update_agent` stop accepting `tradingConfig.signalTimeoutMinutes` and `tradingConfig.maxEntryDeviationAtrMultiple` on the shared `.strict()` `TradingConfigSchema`, so a client still sending either is rejected rather than ignored — the same input-acceptance narrowing that made v4, v5, v6, v8, v10, v23 and v24 majors. Neither has a replacement key: one `platform_config` value governs the entry-price drift budget for every decision (read at evaluation time, so an admin edit applies to the next evaluation), and one governs how long an entry may stay unfilled (snapshotted onto the position at creation, so an edit can never cancel an order already resting on the book). On the read side `AgentTradingConfigDTO` drops both fields on every agent-returning tool, and so do the explorer trading spec and the agent-review payload; `get_trading_config_catalog` drops `defaultSignalTimeoutMinutes` and the `minimum_`/`maximum_maxEntryDeviationAtrMultiple` bound pair, while `defaultMaxEntryDeviationAtrMultiple` and `defaultTtlMinutes` stay and become the values that actually govern. Behaviourally a conversational entry and an autonomous entry on the same setup now receive the **identical** unfilled lifetime — the mode-selecting fallback that chose between a per-agent timeout and a hardcoded 15-minute resting window is gone, and the three-way timeout enum with it. Never published as a package version |
519
762
  | **31.0.0** | **Proxy change, and the end of the pairing rule.** The version announced downstream is now read from the upstream handshake at connect time and relayed verbatim, instead of being a constant compiled into this package. A local client reads the contract it will actually reach, on every connection, with no release involved. Breaking because the package number now means something different — this proxy's own code, not the server's contract — so `npm view` and the handshake legitimately differ, and code keyed to them being equal is wrong. Retired with it: the publish-time deploy gate (`scripts/assert-deployed-contract.mjs`) and the `MAJOR.MINOR` pairing rule, both of which existed only because the two numbers could disagree. Fails closed if a connected server announces no `serverInfo` rather than substituting its own version. Contract moves no longer produce a release here |
520
- | 32.0.0 | Server contract v32.0.0, **breaking**: a signal rule flagged `required` at allocation Off is rejected on every rule-writing surface, with a typed error whose `details.inertRequiredSignalIds` names every offending signal. An input-acceptance narrowing invisible in the published schema (conversion drops effects) and unmigrated in storage, so a stored strategy holding the pair is refused on its owner's next write. No proxy code change — the proxy forwards `{ request }` verbatim and relays the announced contract from the handshake |
521
763
 
522
764
  ## Maintainer release procedure
523
765
 
@@ -588,12 +830,29 @@ If a run fails, inspect it before taking action. An `ENEEDAUTH` failure means th
588
830
 
589
831
  ## Skills
590
832
 
591
- Install the BattleGrid skill for AI agent instructions:
833
+ Install the BattleGrid skills for AI agent instructions:
592
834
 
593
835
  ```bash
594
836
  npx skills add playbattlegrid/battlegrid-mcp
595
837
  ```
596
838
 
839
+ Two skills ship from this repo, and both are inside the npm tarball (`SKILL.md`, `skills/`):
840
+
841
+ - **`battlegrid`** — connection, scopes, game play, and the strict compile → review → apply
842
+ strategy workflow.
843
+ - **`battlegrid-strategy-studio`** — full-power strategy authoring, for agents that would
844
+ otherwise compile bare template strategies: custom report sections and system-generated header
845
+ grammar, benchmark sections, condition trees (verdict precedence, `required` enforcement
846
+ gates, `N_OF`/`NOT` groups, condition references), tiered signal weights and the
847
+ weighted-aggregate gate math, ATR trade levels, and post-entry position management. Its
848
+ `references/` carry five validated desk-grade playbooks (volatility-compression breakout,
849
+ crowded-positioning fade, benchmark-gated relative-strength rotation, HTF trend pullback,
850
+ perp/spot flow divergence at structure), copy-adaptable recipes, and process-for-process
851
+ ports of the most popular TradingView community scripts (Squeeze Momentum [LazyBear],
852
+ Supertrend/UT Bot, Chandelier Exit, MACD + 200 MA, golden cross, RSI-2, VWAP reversion,
853
+ Donchian/Turtle, ICT FVG/order blocks) with honest named substitutions where the grammar
854
+ lacks a primitive. Shapes are binding; vocabulary stays live-discovered.
855
+
597
856
  ## License
598
857
 
599
858
  [MIT](LICENSE)
package/SKILL.md ADDED
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: battlegrid
3
+ description: MCP skill for BattleGrid — play crypto prediction games (Market Grid), author trading strategies with the strict compile → review → apply workflow, and manage strategy-bound intelligence agents from AI agents.
4
+ ---
5
+
6
+ # BattleGrid
7
+
8
+ BattleGrid is a real-time cryptocurrency prediction gaming and trading platform. This MCP server gives AI agents access to play games, author trading strategies, and run strategy-bound intelligence agents.
9
+
10
+ ## Discover the live surface first
11
+
12
+ **Tools, prompts, and resources are discovered live from this MCP connection.** Always read the current `tools/list`, `prompts/list`, and `resources/list` before acting — a cached capability list is not authoritative after a server deployment. This skill teaches the workflows and the strict request contracts; it deliberately does not copy the server's tool catalog, metric/transform vocabulary, signal IDs, formulas, or default values. Discover those from the live tools (`list_strategy_categories`, `list_strategy_vocabulary`, `list_strategy_signals`, `get_strategy_signal_definition`, …). Never guess a metric, transform, parameter, template, signal, or enabled-timeframe fact.
13
+
14
+ ## Single-account vs multi-account request shape
15
+
16
+ The strategy-authoring tools — `get_strategy_section_template`, `update_strategy_signal_rule`, `compile_strategy_plan`, `apply_strategy_plan` — use one strict server-owned envelope, `{ request: canonicalPayload }`.
17
+
18
+ - **Single account:** call them as the server publishes them, e.g. `compile_strategy_plan({ request })`.
19
+ - **Multiple accounts (proxy):** live discovery adds a sibling `account`, so the shape is exactly `{ account, request }`. Select the account in the outer field; keep `request` exactly as discovered. The proxy strips only `account` and forwards the unchanged `{ request }`.
20
+
21
+ Never put `account` inside `request`, and never flatten request fields beside it. Other tools keep whatever input shape live discovery reports for them.
22
+
23
+ ## Scopes
24
+
25
+ - `mcp:read` — strategy discovery **and** non-financial configuration writes (author strategies, edit agents, customize signals). Treat it as configuration authority, not view-only.
26
+ - `mcp:wager` — financial actions (submit paid entries, accept/cancel entry decisions, deployment policies). Enable **Server-Signed Wagers** in Profile → MCP to grant it. Pending entry decisions come from the conversational surface, which waits for approval; an agent deployed to a radar coin or a trading-enabled arena slot executes without one.
27
+
28
+ ## Author a strategy: compile → review → apply
29
+
30
+ Strategies are authored through one strict, whole-plan workflow. **Compilation changes no strategy, agent or revision; `apply_strategy_plan` is the only write to the strategy itself** — but compile is not read-only either: it parks the plan its own apply reads, and each call mints a distinct record and token, so compile once per reviewed payload and never retry or parallelise it. Review the exact returned plan before confirming.
31
+
32
+ 1. **Choose the operation and revision.**
33
+ - `list_strategies({ includeInactive? })` — visible SYSTEM and owned PRIVATE strategies with lifecycle, quota, usage, and revisions. Use `includeInactive:true` to prepare a RESTORE.
34
+ - `get_strategy({ strategyId, includeInactive? })` — the complete report, dense signal scorecard, gates, usage, and current `revision`. Thread every returned revision into the next revisioned call.
35
+ 2. **Discover the report vocabulary progressively.**
36
+ - `list_strategy_categories()` → `list_strategy_vocabulary({ category })` → `get_metric_construction_hints({ metric })` → `get_strategy_column_contract({ column, sectionTimeframe? })`.
37
+ - `get_strategy_section_template({ request })` for a listed template; `preview_strategy_report(payload)` for a point-in-time rendered preview.
38
+ 3. **Discover signals at the strategy timeframe.**
39
+ - `list_strategy_signals({ module?, query? })` → `get_strategy_signal_definition({ signalId, timeframe })`. Availability is structural, not a promise of a live trigger.
40
+ 4. **(Optional) review draft-only guidance.**
41
+ - `derive_strategy_rule_view({ sections, rules? })` returns report perception, server defaults, and suggestions without reading or writing a strategy. Suggestions/resets only shape the next plan input; they persist only through `apply_strategy_plan`.
42
+ 5. **Compile one complete plan.**
43
+ - `compile_strategy_plan({ request })`, where the nested request contains exactly one strict branch plus a bounded `coinSelection`, `intentSummary`, and `assumptions`:
44
+ - **CREATE** — supplies the full new aggregate.
45
+ - **UPDATE** — supplies at least one changed axis and `expectedRevision`. **Send only the
46
+ axes that change.** The server preserves every axis you omit and every signal you do not
47
+ name, and it re-derives the complete post-state, scorecard and diff itself. Restating the
48
+ whole post-state changes nothing about the result and the account pays for those tokens on
49
+ this call and on every later step of the conversation. Three fields are *not* axes and are
50
+ required on every compile regardless — `intentSummary`, `assumptions` and `coinSelection`;
51
+ omitting one is a typed error, not a saving.
52
+ - **RESTORE** — targets an owned inactive revision and may include repair axes.
53
+ - Signal overrides are sparse: an omitted signal/axis stays unchanged; omitted `params` preserves canonical params byte-for-byte; present `params` replaces them only after strict validation.
54
+ - **`required` needs a weight.** A rule with `required: true` at `allocation: 0` is rejected (contract 34): the scorecard's triggered set excludes Off, so the flag could satisfy no gate and block no trade. Either raise the allocation or leave `required: false` — the server picks neither for you. The refusal names every offending signal in `details.inertRequiredSignalIds`, and it can fire on rules you did not touch: a strategy authored before contract 34 may hold the pair, and the merged post-state is what gets checked.
55
+ 6. **Review before confirming.**
56
+ - `approvedPlan` — complete post-state, proposed revision, dense scorecard, viability, canonical diff, expiry, and bound-agent impact.
57
+ - `reviewContext` — exact column contracts, point-in-time report preview and coin scope, open-position observation, and provisional quota/name admission (advisory until the write). Open positions are awareness only and do not block an edit.
58
+ - The plan token expires after five minutes. Recompile after expiry, catalog drift, revision drift, or a changed bound-agent fence.
59
+ 7. **Apply the plan the server already holds.**
60
+ - After explicit user approval: `apply_strategy_plan({ request: { planToken, confirm: true } })`. **There is no `plan` member** — one is rejected as an unknown key. The server keeps the plan its own compile approved and reads it back, so nothing is copied from the compile response and nothing can be mistyped, truncated or half-reconstructed in transit.
61
+ - Forward `planToken` byte-for-byte exactly as compile returned it. It is an opaque signed value: never retyped, paraphrased, abbreviated or rebuilt from memory. A mangled token addresses no approved plan and is refused.
62
+ - Refusals and their recoveries: `PLAN_APPROVAL_NOT_FOUND` means no approved plan answers to this token — it was already applied, it lapsed, or it was never issued; compile again. `TOKEN_EXPIRED` means the five minutes ran out; compile again. Anything else — a quota, a name collision, a bound agent that changed, a moved catalog — **leaves the plan applicable**: clear the cause and confirm again with the same token while it lives.
63
+ - Changed configuration reaches every bound agent immediately. Report the returned strategy, committed revision, changed axes and applied impact exactly as given, and use that returned revision for the next mutation.
64
+
65
+ **Focused edits & lifecycle:** `update_strategy_signal_rule({ request })` is the thin one-rule edit (requires `required`; omit `params` to preserve them; `required: true` needs `allocation > 0`). `fork_strategy` requires `sourceRevision`; `archive_strategy` requires `expectedRevision` and `confirm:true`; `restore_strategy` is only the thin unchanged-content path — if it reports `REPAIR_REQUIRED`, use the RESTORE compile/review/apply flow instead.
66
+
67
+ **Author the full surface, not a template.** A bare compile — a few platform sections, no conditions, untouched signal weights — uses a fraction of the studio: the strategy aggregate also owns typed conditions (verdicts + `required` enforcement gates), tiered signal allocations with per-signal params, routing gates (`minAggregateScore`, `minRequiredCount`, `minAtrPct`), the ATR stop band + risk-reward floor, post-entry position management (break-even / trailing / time-decay), and marker-bearing Market Read prose. The companion **`battlegrid-strategy-studio` skill** (shipped beside this one) carries the capability map plus validated desk-grade playbooks and recipes for all of it — activate it whenever a strategy is being created or upgraded.
68
+
69
+ ## Strategy-bound agents
70
+
71
+ Bind a strategy to an agent at creation time — there is no direct strategy-creation tool.
72
+
73
+ 1. `list_approved_models()` — valid `modelId` values for agent creation/update.
74
+ 2. `list_strategies()` — pick the `strategyId` to bind.
75
+ 3. `create_intelligence_agent({ …, modelId, strategyId })` — create a strategy-bound agent (avatar is server-minted).
76
+ 4. `update_intelligence_agent({ agentId, … })` — update config; rebinding via `strategyId` requires `confirm:true`.
77
+ 5. `get_agent_journal({ agentId })` / `get_agent_automation_status({ agentId })` — monitor performance and deployments.
78
+
79
+ ## Play a game (Market Grid)
80
+
81
+ Predict UP or DOWN for each coin in the pool; exactly one coin is your **Captain** (2x score multiplier). Drive it from the live tools:
82
+
83
+ 1. `get_account_state()` — balance, rank, agent slots, wager status.
84
+ 2. `list_market_grid_sessions({ status: "PENDING" })` — find an open game (a `$0` entry fee is risk-free).
85
+ 3. `get_market_grid_session({ sessionId })` — coin pool, timeframe, payout structure.
86
+ 4. `get_market_context({ sessionId })` — indicators, rankings, and trends for the session.
87
+ 5. `check_market_grid_submission({ sessionId })` — avoid duplicate submissions (`update_market_grid` to modify).
88
+ 6. `submit_market_grid({ sessionId, grid, reasoning, confidenceScore, modelName, pickReasoning })` — submit predictions.
89
+ 7. `get_market_grid_results({ sessionId })` — results once the session is `SETTLED`.
90
+
91
+ **Grid validation:** grid size matches the coin pool; each coin appears once; positions are sequential (0,1,2,…); exactly one cell is `isCaptain: true`.
92
+
93
+ The `play-market-grid` prompt (discover via `prompts/list`) provides a guided end-to-end workflow.
94
+
95
+ ## Retired operations
96
+
97
+ `create_strategy` (and other legacy direct-authoring/agent-scoped rule tools) are **retired** — they are absent from discovery and cannot be invoked. Author strategies with compile → review → apply, edit rules with `update_strategy_signal_rule`, and bind strategies to agents at agent creation. Do not attempt flat legacy payloads; the server enforces a closed-world request root and the proxy never reconstructs them.
98
+
99
+ ## Common errors
100
+
101
+ | Error | Cause | Fix |
102
+ |-------|-------|-----|
103
+ | `BATTLEGRID_API_KEY is required` | Missing API key | Set `BATTLEGRID_API_KEY` (or `BATTLEGRID_API_KEYS`) |
104
+ | `API key must start with "bg_live_"` | Invalid key format | Generate a new key at battlegrid.trade → Profile → MCP |
105
+ | Authentication failed (401/403) | Key revoked/rotated | Generate a new key and **restart** the proxy (keys read once at startup) |
106
+ | `"account" parameter is required` | Multi-account call missing `account` | Add the outer `account`; keep `request` unchanged |
107
+ | Plan token expired / revision drift | >5 min since compile, or upstream changed | Recompile, review the fresh plan, then apply |
108
+ | Required at allocation Off | A rule flags `required` on a signal weighted `0` (contract 34) | Read `details.inertRequiredSignalIds`; per signal either raise `allocation` or set `required: false` |
109
+ | Method not found | Calling a retired/unknown tool | Re-run `tools/list`; use the compile → review → apply flow |
110
+ | `Wager scope required` | `mcp:wager` not enabled | Enable Server-Signed Wagers in Profile → MCP |
package/dist/index.d.ts CHANGED
@@ -49,7 +49,7 @@ import { type Implementation } from '@modelcontextprotocol/sdk/types.js';
49
49
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
50
50
  * correction. Never move it to track the server.
51
51
  */
52
- export declare const PACKAGE_VERSION = "31.0.3";
52
+ export declare const PACKAGE_VERSION = "31.1.5";
53
53
  export declare const DEFAULT_URL = "https://mcp.battlegrid.trade/mcp";
54
54
  export interface EnvConfig {
55
55
  apiKeys: string[];
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema
52
52
  * being asked. Move it for a change to THIS package — a proxy fix, a dependency bump, a docs
53
53
  * correction. Never move it to track the server.
54
54
  */
55
- export const PACKAGE_VERSION = '31.0.3';
55
+ export const PACKAGE_VERSION = '31.1.5';
56
56
  export const DEFAULT_URL = 'https://mcp.battlegrid.trade/mcp';
57
57
  const MAX_RETRIES = 3;
58
58
  const RETRY_DELAYS_MS = [2000, 4000, 8000];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@battlegrid/mcp-server",
3
- "version": "31.0.3",
3
+ "version": "31.1.5",
4
4
  "description": "BattleGrid MCP server — play crypto prediction games from AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,9 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "README.md",
13
- "LICENSE"
13
+ "LICENSE",
14
+ "SKILL.md",
15
+ "skills"
14
16
  ],
15
17
  "scripts": {
16
18
  "build": "tsc",