meguro-mcp 0.2.1 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `meguro-mcp` are recorded here. Versions follow Semantic Versioning.
4
4
 
5
+ ## 0.2.3 — 2026-08-01
6
+
7
+ - Adds `templates_list`, a read-only closed-world projection of the ordered practice-store template
8
+ catalog, so customers can discover supported templates without duplicating registry data.
9
+ - Preserves only the documented public template fields and excludes profiles, scenario presets,
10
+ credentials, and private route payload from MCP responses.
11
+ - Pins Console connection guidance to the exact `meguro-mcp@0.2.3` artifact.
12
+
13
+ ## 0.2.2 — 2026-08-01
14
+
15
+ - Enforces each tool's declared top-level argument boundary before API transport, with bounded errors
16
+ that do not echo attacker-controlled property names.
17
+ - Rejects practice-store ids and oversized or malformed practice-run ids locally across run,
18
+ continuation, and Exam entry tools while preserving supported historical run-id shapes.
19
+ - Keeps well-formed missing and cross-workspace run ids on the same generic
20
+ `practice-run-unavailable` response so validation does not create an existence oracle.
21
+
5
22
  ## 0.2.1 — 2026-08-01
6
23
 
7
24
  - Preserves the API's approved wrong/revoked workspace-key code and actionable Connection settings
package/README.md CHANGED
@@ -14,7 +14,7 @@ Dependency-free, no build step: the server is plain Node ≥ 20.
14
14
  Customer MCP clients run the exact public version directly from npm:
15
15
 
16
16
  ```bash
17
- npx -y meguro-mcp@0.2.1
17
+ npx -y meguro-mcp@0.2.3
18
18
  ```
19
19
 
20
20
  Pin the version in client configuration. A pinned quickstart stays reproducible and never changes its
@@ -32,11 +32,12 @@ Console provides fast trusted proof and evidence inspection; the agency agent no
32
32
 
33
33
  <!-- BEGIN GENERATED MCP TOOL TABLE -->
34
34
  <!-- Run `npm run docs:tools --workspace meguro-mcp` to refresh this section from `tools/list`. -->
35
- This table is generated from the server's live `tools/list` response (43 tools).
35
+ This table is generated from the server's live `tools/list` response (44 tools).
36
36
 
37
37
  | Tool | Title | Behavior annotations |
38
38
  |---|---|---|
39
39
  | `docs_read` | Read Meguro receipt and Gate documentation | read-only · non-destructive · idempotent · closed-world |
40
+ | `templates_list` | List practice-store templates | read-only · non-destructive · idempotent · closed-world |
40
41
  | `stores_list` | List practice stores | read-only · non-destructive · idempotent · closed-world |
41
42
  | `store_create` | Create a practice store | read-write · non-destructive · non-idempotent · closed-world |
42
43
  | `store_delete` | Delete a practice store | read-write · destructive · non-idempotent · closed-world |
@@ -161,7 +162,7 @@ claude mcp add meguro \
161
162
  -e MEGURO_API_BASE_URL=https://api-dev.meguro.io \
162
163
  -e MEGURO_API_TOKEN=meg_sk_... \
163
164
  -e MEGURO_DASHBOARD_URL=https://... \
164
- -- npx -y meguro-mcp@0.2.1
165
+ -- npx -y meguro-mcp@0.2.3
165
166
  ```
166
167
 
167
168
  ## Register — Cursor (`.cursor/mcp.json`)
@@ -171,7 +172,7 @@ claude mcp add meguro \
171
172
  "mcpServers": {
172
173
  "meguro": {
173
174
  "command": "npx",
174
- "args": ["-y", "meguro-mcp@0.2.1"],
175
+ "args": ["-y", "meguro-mcp@0.2.3"],
175
176
  "env": {
176
177
  "MEGURO_API_BASE_URL": "https://api-dev.meguro.io",
177
178
  "MEGURO_API_TOKEN": "meg_sk_...",
@@ -192,6 +193,6 @@ npm test --workspace meguro-mcp
192
193
  ```
193
194
 
194
195
  The test suite packs the public artifact, installs it into a temporary clean-room project, starts the
195
- installed binary, and requires `tools/list` to return exactly 43 tools without leaking supplied
196
+ installed binary, and requires `tools/list` to return exactly 44 tools without leaking supplied
196
197
  credentials to stdout or stderr. Maintainers should follow [RELEASING.md](./RELEASING.md) for every
197
198
  version; release notes are in [CHANGELOG.md](./CHANGELOG.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meguro-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Meguro control-plane MCP server: drive worlds, runs, and verdicts from your own AI tools.",
package/src/server.mjs CHANGED
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline';
7
7
  import { documentationResource, documentationResources } from './docs.mjs';
8
8
  import { createTools, redactSecrets } from './tools.mjs';
9
9
 
10
- const SERVER_INFO = { name: 'meguro', version: '0.2.1' };
10
+ const SERVER_INFO = { name: 'meguro', version: '0.2.3' };
11
11
  const PROTOCOL_VERSION = '2025-03-26';
12
12
 
13
13
  const tools = createTools({
package/src/tools.mjs CHANGED
@@ -11,18 +11,20 @@ const SECRET_HEADER = /\b(?:authorization|cookie|set-cookie|x-shopify-access-tok
11
11
  const SECRET_KEY = /(?:authorization|cookie|password|secret|token|api[_-]?key|signedsnapshot)/iu;
12
12
  const PRIVATE_RESPONSE_KEY = /^(?:raw|rawBody|rawBodyPreview|responsePayload|requestHeaders|responseHeaders|headers|cookies)$/iu;
13
13
  const RESERVED_WORLD_IDS = new Set(['api', 'api-dev', 'www', 'dev', 'hooks', 'control']);
14
+ const PRACTICE_ATTEMPT_ID = /^pa-[a-z0-9][a-z0-9-]{0,124}$/u;
14
15
  const AUTH_TEACHING = Object.freeze({
15
16
  'workspace-api-key-invalid': 'This workspace API key is invalid or revoked. Create or replace it in Connection settings.',
16
17
  'workspace-api-key-revoked': 'This workspace API key is revoked. Create or replace it in Connection settings.',
17
18
  });
18
19
  const FLEET_TOOL_NAMES = new Set([
19
- 'stores_list', 'store_create', 'store_delete', 'store_passport',
20
+ 'templates_list', 'stores_list', 'store_create', 'store_delete', 'store_passport',
20
21
  'workspaces_list', 'workspace_create', 'workspace_archive', 'workspace_unarchive',
21
22
  'share_create', 'shares_list', 'share_status', 'share_publish', 'share_revoke',
22
23
  'catalog_slice_read', 'catalog_slice_snapshot', 'catalog_slices_saved',
23
24
  'store_claim_by_code', 'store_claim',
24
25
  ]);
25
26
  const TOOL_PRESENTATION = Object.freeze({
27
+ templates_list: { title: 'List practice-store templates', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
26
28
  stores_list: { title: 'List practice stores', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
27
29
  store_create: { title: 'Create a practice store', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
28
30
  store_delete: { title: 'Delete a practice store', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
@@ -152,6 +154,47 @@ function catalogSnapshotProjection(value) {
152
154
  : safe;
153
155
  }
154
156
 
157
+ const STORE_TEMPLATE_FIELDS = Object.freeze([
158
+ 'key',
159
+ 'label',
160
+ 'category',
161
+ 'recommended',
162
+ 'bestFor',
163
+ 'agentTypes',
164
+ 'agentSees',
165
+ 'recommendedRunDays',
166
+ 'recommendedPaceMinutes',
167
+ 'recommendedStepDays',
168
+ 'eventWindowReason',
169
+ 'shortDebugDays',
170
+ 'extendedRunDays',
171
+ 'whatAgentGets',
172
+ 'supportedReads',
173
+ 'liveRun',
174
+ 'goodFirstAgents',
175
+ 'supportedWrites',
176
+ 'knownUnsupported',
177
+ 'goodAgentShould',
178
+ 'avoid',
179
+ 'reportGrades',
180
+ ]);
181
+
182
+ function storeTemplatesProjection(value) {
183
+ if (!Array.isArray(value?.storeTemplates)) {
184
+ throw new Error('Meguro template catalog response did not contain storeTemplates');
185
+ }
186
+ return {
187
+ storeTemplates: value.storeTemplates.map((template) => {
188
+ if (!template || typeof template !== 'object' || Array.isArray(template)) {
189
+ throw new Error('Meguro template catalog contained an invalid template entry');
190
+ }
191
+ return Object.fromEntries(STORE_TEMPLATE_FIELDS
192
+ .filter((field) => Object.hasOwn(template, field))
193
+ .map((field) => [field, secretSafe(template[field], 1)]));
194
+ }),
195
+ };
196
+ }
197
+
155
198
  function practiceErrorResult(status, value, retryAfterSeconds) {
156
199
  const authenticationError = authenticationErrorResult(status, value);
157
200
  if (authenticationError) return authenticationError;
@@ -281,6 +324,14 @@ function requiredPracticeStoreId(args) {
281
324
  return validatedStoreId(canonical, provided[0]);
282
325
  }
283
326
 
327
+ function requiredPracticeAttemptId(args, key = 'attemptId') {
328
+ const attemptId = requiredString(args, key);
329
+ if (!PRACTICE_ATTEMPT_ID.test(attemptId)) {
330
+ throw new Error(`${key} must be the pa-... run identity returned by practice_run_start, not a practice-store id (storeId)`);
331
+ }
332
+ return attemptId;
333
+ }
334
+
284
335
  function optionalPositiveInteger(args, key) {
285
336
  const value = args?.[key];
286
337
  if (value === undefined) return undefined;
@@ -1005,6 +1056,18 @@ export function createTools(config) {
1005
1056
  required: ['topic', 'version'],
1006
1057
  },
1007
1058
  },
1059
+ {
1060
+ name: 'templates_list',
1061
+ description: 'List the ordered, public-safe practice-store template catalog available for creating a store. The response is a bounded projection of storeTemplates only: it excludes customer profiles, scenario presets, credentials, and private route payload. An OAuth grant may select an owned non-default workspace; an API key remains bound to its own workspace.',
1062
+ inputSchema: {
1063
+ type: 'object',
1064
+ additionalProperties: false,
1065
+ properties: {
1066
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional OAuth selector for an account-owned non-default workspace. A workspace API key may omit it or repeat only its own binding.' },
1067
+ },
1068
+ required: [],
1069
+ },
1070
+ },
1008
1071
  {
1009
1072
  name: 'stores_list',
1010
1073
  description: 'List one workspace\'s practice-store fleet with ids, names, templates, generated-history span, readiness, active-run state, and deletion context. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace.',
@@ -1358,7 +1421,7 @@ export function createTools(config) {
1358
1421
  inputSchema: {
1359
1422
  type: 'object', additionalProperties: false,
1360
1423
  properties: {
1361
- attemptId: { type: 'string', minLength: 1, description: 'Completed practice-run attempt id carrying immutable Exam capture.' },
1424
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Completed practice-run attempt id carrying immutable Exam capture.' },
1362
1425
  shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Exact lowercase connected Shopify development-store domain.' },
1363
1426
  },
1364
1427
  required: ['attemptId', 'shopDomain'],
@@ -1370,7 +1433,7 @@ export function createTools(config) {
1370
1433
  inputSchema: {
1371
1434
  type: 'object', additionalProperties: false,
1372
1435
  properties: {
1373
- attemptId: { type: 'string', minLength: 1, description: 'Completed practice-run attempt id carrying immutable Exam capture.' },
1436
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Completed practice-run attempt id carrying immutable Exam capture.' },
1374
1437
  shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Exact lowercase connected Shopify development-store domain; this is the typed target confirmation.' },
1375
1438
  },
1376
1439
  required: ['attemptId', 'shopDomain'],
@@ -1403,7 +1466,7 @@ export function createTools(config) {
1403
1466
  properties: {
1404
1467
  storeId: { type: 'string', minLength: 1, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
1405
1468
  worldId: { type: 'string', minLength: 1, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
1406
- continueFromAttemptId: { type: 'string', minLength: 1, description: 'Completed prior segment to continue on the same aging practice store. Available only when the server tier policy allows continuation.' },
1469
+ continueFromAttemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Completed prior segment to continue on the same aging practice store. Available only when the server tier policy allows continuation.' },
1407
1470
  clock: { type: 'object', description: 'Existing hosted practice-run clock contract.' },
1408
1471
  externalAgent: { type: 'object', description: 'Optional bounded external-agent identity metadata.' },
1409
1472
  subject: {
@@ -1429,7 +1492,7 @@ export function createTools(config) {
1429
1492
  description: 'Read the server-authoritative state of an external-agent practice run, including its announced run-conclusion deadlines and immutable conclusion receipt after any exit. Use Console for quick proof; the tested agent remains in the caller\'s environment.',
1430
1493
  inputSchema: {
1431
1494
  type: 'object', additionalProperties: false,
1432
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1495
+ properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1433
1496
  required: ['attemptId'],
1434
1497
  },
1435
1498
  },
@@ -1438,7 +1501,7 @@ export function createTools(config) {
1438
1501
  description: 'Capture an immutable checkpoint for an external-agent practice run and return a bounded evidence summary. Console is quick proof; agent execution remains in the caller\'s environment.',
1439
1502
  inputSchema: {
1440
1503
  type: 'object', additionalProperties: false,
1441
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1504
+ properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1442
1505
  required: ['attemptId'],
1443
1506
  },
1444
1507
  },
@@ -1449,7 +1512,7 @@ export function createTools(config) {
1449
1512
  type: 'object',
1450
1513
  additionalProperties: false,
1451
1514
  properties: {
1452
- attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
1515
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
1453
1516
  days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
1454
1517
  until: { type: 'object', description: 'Existing meguro.practice-run-until.v1 event-gated request.' },
1455
1518
  expectedDay: { type: 'integer', minimum: 0, description: 'Current practiceRun.time.watermarkDay.' },
@@ -1464,7 +1527,7 @@ export function createTools(config) {
1464
1527
  description: 'Explicitly finish an external-agent practice run without inventing additional time or evidence. The response includes the immutable conclusion receipt and echoes the deadlines announced at start. Console is quick proof; agent execution remains in the caller\'s environment.',
1465
1528
  inputSchema: {
1466
1529
  type: 'object', additionalProperties: false,
1467
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1530
+ properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1468
1531
  required: ['attemptId'],
1469
1532
  },
1470
1533
  },
@@ -1473,7 +1536,7 @@ export function createTools(config) {
1473
1536
  description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. `report` is the protocol/route compatibility name for the receipt (the tool name and the `operation: "report"` field keep it for wire stability); user-facing language is "receipt". The payload includes an unguessable public receipt id, canonical unauthenticated fetch path, and SHA-256: gate a deploy by fetching that id and verifying the exact bytes before promotion. Raw private request and response payloads stay out of MCP; use Console for quick proof while the agent remains in the caller\'s environment. Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
1474
1537
  inputSchema: {
1475
1538
  type: 'object', additionalProperties: false,
1476
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1539
+ properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1477
1540
  required: ['attemptId'],
1478
1541
  },
1479
1542
  },
@@ -1482,7 +1545,7 @@ export function createTools(config) {
1482
1545
  description: 'Read the impact receipt for a completed external-agent practice run: the base year — the same store, same days, without your agent — compared with your agent\'s year, showing what changed because of your agent (orders, units, and revenue), a footprint of every write, and an integrity gate that reports a reason instead of numbers when the comparison is not clean. The payload includes an unguessable public receipt id, canonical unauthenticated fetch path, and SHA-256: gate a deploy by fetching that id and verifying the exact bytes before promotion. Read-only; never mutates the run and returns no credentials. Takes the run\'s attemptId (from practice_run_start), not the practice-store id (storeId). Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
1483
1546
  inputSchema: {
1484
1547
  type: 'object', additionalProperties: false,
1485
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1548
+ properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1486
1549
  required: ['attemptId'],
1487
1550
  },
1488
1551
  },
@@ -1535,9 +1598,25 @@ export function createTools(config) {
1535
1598
  const { title, ...annotations } = presentation;
1536
1599
  return { ...definition, title, annotations };
1537
1600
  });
1601
+ const definitionsByName = new Map(definitions.map((definition) => [definition.name, definition]));
1602
+
1603
+ function validateTopLevelArguments(name, args) {
1604
+ const definition = definitionsByName.get(name);
1605
+ if (!definition) return;
1606
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
1607
+ throw new Error(`${name} arguments must be an object`);
1608
+ }
1609
+ if (definition.inputSchema.additionalProperties !== false) return;
1610
+ const allowed = new Set(Object.keys(definition.inputSchema.properties ?? {}));
1611
+ const unexpected = Object.keys(args).filter((key) => !allowed.has(key)).sort();
1612
+ if (unexpected.length > 0) {
1613
+ throw new Error(`${name} arguments must match the declared top-level properties: ${[...allowed].join(', ') || '(none)'}`);
1614
+ }
1615
+ }
1538
1616
 
1539
1617
  async function call(name, args = {}) {
1540
1618
  try {
1619
+ validateTopLevelArguments(name, args);
1541
1620
  switch (name) {
1542
1621
  case 'docs_read': {
1543
1622
  const topic = requiredString(args, 'topic');
@@ -1549,6 +1628,11 @@ export function createTools(config) {
1549
1628
  }
1550
1629
  return textResult(documentationByTopic(topic, args.version));
1551
1630
  }
1631
+ case 'templates_list': {
1632
+ const workspaceId = optionalWorkspaceId(args);
1633
+ const response = await fleetRequest(name, 'GET', '/practice/profiles', undefined, { workspaceId });
1634
+ return 'error' in response ? response.error : textResult(storeTemplatesProjection(response.value));
1635
+ }
1552
1636
  case 'stores_list': {
1553
1637
  const workspaceId = optionalWorkspaceId(args);
1554
1638
  const response = await fleetRequest(name, 'GET', '/practice/stores', undefined, { workspaceId });
@@ -1819,13 +1903,13 @@ export function createTools(config) {
1819
1903
  )));
1820
1904
  }
1821
1905
  case 'exam_preflight': {
1822
- const attemptId = requiredString(args, 'attemptId');
1906
+ const attemptId = requiredPracticeAttemptId(args);
1823
1907
  const shopDomain = requiredShopifyDevDomain(args);
1824
1908
  const response = await examRequest(name, 'POST', '/practice-exams/preflight', { attemptId, shopDomain }, attemptId);
1825
1909
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
1826
1910
  }
1827
1911
  case 'exam_start': {
1828
- const attemptId = requiredString(args, 'attemptId');
1912
+ const attemptId = requiredPracticeAttemptId(args);
1829
1913
  const shopDomain = requiredShopifyDevDomain(args);
1830
1914
  const created = await examRequest(name, 'POST', '/practice-exams', { attemptId, shopDomain }, attemptId);
1831
1915
  if ('error' in created) return created.error;
@@ -1910,7 +1994,7 @@ export function createTools(config) {
1910
1994
  const storeId = requiredPracticeStoreId(args);
1911
1995
  const body = {
1912
1996
  purpose: 'external-agent',
1913
- ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: args.continueFromAttemptId } : {}),
1997
+ ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
1914
1998
  ...(args.clock !== undefined ? { clock: args.clock } : {}),
1915
1999
  ...(args.externalAgent !== undefined ? { externalAgent: args.externalAgent } : {}),
1916
2000
  ...(args.subject !== undefined ? { subject: args.subject } : {}),
@@ -1934,7 +2018,7 @@ export function createTools(config) {
1934
2018
  }));
1935
2019
  }
1936
2020
  case 'practice_run_status': {
1937
- const attemptId = requiredString(args, 'attemptId');
2021
+ const attemptId = requiredPracticeAttemptId(args);
1938
2022
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`);
1939
2023
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1940
2024
  const state = response.json ?? {};
@@ -1946,7 +2030,7 @@ export function createTools(config) {
1946
2030
  }));
1947
2031
  }
1948
2032
  case 'practice_run_checkpoint': {
1949
- const attemptId = requiredString(args, 'attemptId');
2033
+ const attemptId = requiredPracticeAttemptId(args);
1950
2034
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {});
1951
2035
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1952
2036
  return textResult(secretSafe({
@@ -1958,7 +2042,7 @@ export function createTools(config) {
1958
2042
  }));
1959
2043
  }
1960
2044
  case 'practice_run_advance': {
1961
- const attemptId = requiredString(args, 'attemptId');
2045
+ const attemptId = requiredPracticeAttemptId(args);
1962
2046
  const hasDays = args.days !== undefined;
1963
2047
  const hasUntil = args.until !== undefined;
1964
2048
  if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
@@ -1983,7 +2067,7 @@ export function createTools(config) {
1983
2067
  }));
1984
2068
  }
1985
2069
  case 'practice_run_finish': {
1986
- const attemptId = requiredString(args, 'attemptId');
2070
+ const attemptId = requiredPracticeAttemptId(args);
1987
2071
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/finish`, {});
1988
2072
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1989
2073
  return textResult(secretSafe({
@@ -1994,7 +2078,7 @@ export function createTools(config) {
1994
2078
  }));
1995
2079
  }
1996
2080
  case 'practice_run_report': {
1997
- const attemptId = requiredString(args, 'attemptId');
2081
+ const attemptId = requiredPracticeAttemptId(args);
1998
2082
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`);
1999
2083
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
2000
2084
  return textResult({
@@ -2004,7 +2088,7 @@ export function createTools(config) {
2004
2088
  });
2005
2089
  }
2006
2090
  case 'practice_run_impact': {
2007
- const attemptId = requiredString(args, 'attemptId');
2091
+ const attemptId = requiredPracticeAttemptId(args);
2008
2092
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`);
2009
2093
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
2010
2094
  // T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim