meguro-mcp 0.2.0 → 0.2.2

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.2 — 2026-08-01
6
+
7
+ - Enforces each tool's declared top-level argument boundary before API transport, with bounded errors
8
+ that do not echo attacker-controlled property names.
9
+ - Rejects practice-store ids and oversized or malformed practice-run ids locally across run,
10
+ continuation, and Exam entry tools while preserving supported historical run-id shapes.
11
+ - Keeps well-formed missing and cross-workspace run ids on the same generic
12
+ `practice-run-unavailable` response so validation does not create an existence oracle.
13
+
14
+ ## 0.2.1 — 2026-08-01
15
+
16
+ - Preserves the API's approved wrong/revoked workspace-key code and actionable Connection settings
17
+ guidance through MCP tool errors while keeping malformed authentication responses fail-closed.
18
+ - Keeps authentication failures secret-free, single-attempt, and explicitly classified as HTTP 401
19
+ or 403 without changing unrelated tool-error projections.
20
+ - Pins Console and customer scenario fixtures to the exact `meguro-mcp@0.2.1` artifact.
21
+
5
22
  ## 0.2.0 — 2026-08-01
6
23
 
7
24
  - Publishes the frozen 43-tool control-plane registry already present in the repository, without
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.0
17
+ npx -y meguro-mcp@0.2.2
18
18
  ```
19
19
 
20
20
  Pin the version in client configuration. A pinned quickstart stays reproducible and never changes its
@@ -161,7 +161,7 @@ claude mcp add meguro \
161
161
  -e MEGURO_API_BASE_URL=https://api-dev.meguro.io \
162
162
  -e MEGURO_API_TOKEN=meg_sk_... \
163
163
  -e MEGURO_DASHBOARD_URL=https://... \
164
- -- npx -y meguro-mcp@0.2.0
164
+ -- npx -y meguro-mcp@0.2.2
165
165
  ```
166
166
 
167
167
  ## Register — Cursor (`.cursor/mcp.json`)
@@ -171,7 +171,7 @@ claude mcp add meguro \
171
171
  "mcpServers": {
172
172
  "meguro": {
173
173
  "command": "npx",
174
- "args": ["-y", "meguro-mcp@0.2.0"],
174
+ "args": ["-y", "meguro-mcp@0.2.2"],
175
175
  "env": {
176
176
  "MEGURO_API_BASE_URL": "https://api-dev.meguro.io",
177
177
  "MEGURO_API_TOKEN": "meg_sk_...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meguro-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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.0' };
10
+ const SERVER_INFO = { name: 'meguro', version: '0.2.2' };
11
11
  const PROTOCOL_VERSION = '2025-03-26';
12
12
 
13
13
  const tools = createTools({
package/src/tools.mjs CHANGED
@@ -11,6 +11,11 @@ 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;
15
+ const AUTH_TEACHING = Object.freeze({
16
+ 'workspace-api-key-invalid': 'This workspace API key is invalid or revoked. Create or replace it in Connection settings.',
17
+ 'workspace-api-key-revoked': 'This workspace API key is revoked. Create or replace it in Connection settings.',
18
+ });
14
19
  const FLEET_TOOL_NAMES = new Set([
15
20
  'stores_list', 'store_create', 'store_delete', 'store_passport',
16
21
  'workspaces_list', 'workspace_create', 'workspace_archive', 'workspace_unarchive',
@@ -93,6 +98,33 @@ function secretSafe(value, depth = 0) {
93
98
  return value;
94
99
  }
95
100
 
101
+ function authenticationErrorResult(status, value) {
102
+ if (status !== 401 && status !== 403) return null;
103
+ const body = value && typeof value === 'object' ? value : {};
104
+ const candidates = [body.practiceRunError, body.error, body];
105
+ const source = candidates.find((candidate) => candidate && typeof candidate === 'object'
106
+ && typeof candidate.code === 'string' && typeof candidate.message === 'string');
107
+ const routeCode = source?.code
108
+ ?? (typeof body.code === 'string' ? body.code : undefined)
109
+ ?? (typeof body.error === 'string' && body.error !== 'unauthorized' ? body.error : undefined);
110
+ if (routeCode && !Object.hasOwn(AUTH_TEACHING, routeCode)) return null;
111
+ const expectedMessage = source ? AUTH_TEACHING[source.code] : undefined;
112
+ const error = expectedMessage && source.message === expectedMessage
113
+ ? { code: source.code, message: expectedMessage }
114
+ : { message: 'Meguro practice request failed.' };
115
+ return {
116
+ content: [{ type: 'text', text: JSON.stringify({ httpStatus: status, error }, null, 2) }],
117
+ isError: true,
118
+ };
119
+ }
120
+
121
+ class ToolResultError extends Error {
122
+ constructor(result) {
123
+ super('Meguro request failed.');
124
+ this.result = result;
125
+ }
126
+ }
127
+
96
128
  // A store-deletion preview intentionally returns one narrow capability for the caller to
97
129
  // present back on the second step. Keep every other response field behind the normal secret
98
130
  // scrubber and opt this single, format-checked value back in explicitly.
@@ -122,6 +154,8 @@ function catalogSnapshotProjection(value) {
122
154
  }
123
155
 
124
156
  function practiceErrorResult(status, value, retryAfterSeconds) {
157
+ const authenticationError = authenticationErrorResult(status, value);
158
+ if (authenticationError) return authenticationError;
125
159
  const body = value && typeof value === 'object' ? value : {};
126
160
  const stable = { httpStatus: status };
127
161
  if (body.practiceRunError && typeof body.practiceRunError === 'object') {
@@ -248,6 +282,14 @@ function requiredPracticeStoreId(args) {
248
282
  return validatedStoreId(canonical, provided[0]);
249
283
  }
250
284
 
285
+ function requiredPracticeAttemptId(args, key = 'attemptId') {
286
+ const attemptId = requiredString(args, key);
287
+ if (!PRACTICE_ATTEMPT_ID.test(attemptId)) {
288
+ throw new Error(`${key} must be the pa-... run identity returned by practice_run_start, not a practice-store id (storeId)`);
289
+ }
290
+ return attemptId;
291
+ }
292
+
251
293
  function optionalPositiveInteger(args, key) {
252
294
  const value = args?.[key];
253
295
  if (value === undefined) return undefined;
@@ -664,6 +706,8 @@ export function createTools(config) {
664
706
  let json;
665
707
  try { json = JSON.parse(text); } catch { json = { raw: text }; }
666
708
  if (!response.ok) {
709
+ const authenticationError = authenticationErrorResult(response.status, json);
710
+ if (authenticationError) throw new ToolResultError(authenticationError);
667
711
  const message = json.errors?.[0]?.message ?? json.error ?? text.slice(0, 300);
668
712
  throw new Error(`${response.status} ${path}: ${message}`);
669
713
  }
@@ -786,7 +830,10 @@ export function createTools(config) {
786
830
 
787
831
  async function fleetRequest(toolName, method, path, body, options = {}) {
788
832
  const response = await apiRaw(method, path, body, options);
789
- if (!response.ok) return { error: await fleetErrorResult(toolName, path, response.status, response.json, response.requestId) };
833
+ if (!response.ok) {
834
+ const authenticationError = authenticationErrorResult(response.status, response.json);
835
+ return { error: authenticationError ?? await fleetErrorResult(toolName, path, response.status, response.json, response.requestId) };
836
+ }
790
837
  return { value: response.json };
791
838
  }
792
839
 
@@ -883,7 +930,8 @@ export function createTools(config) {
883
930
  const response = await practiceApi(method, path, body);
884
931
  return response.ok
885
932
  ? { value: response.json ?? {} }
886
- : { error: await examErrorResult(toolName, path, response, attemptId) };
933
+ : { error: authenticationErrorResult(response.status, response.json)
934
+ ?? await examErrorResult(toolName, path, response, attemptId) };
887
935
  }
888
936
 
889
937
  async function examReceiptArtifact(examId, kind, expectedDigest) {
@@ -894,6 +942,8 @@ export function createTools(config) {
894
942
  const text = await response.text();
895
943
  let json;
896
944
  try { json = text ? JSON.parse(text) : null; } catch { json = { examError: { message: 'Meguro returned a non-JSON receipt error.' } }; }
945
+ const authenticationError = authenticationErrorResult(response.status, json);
946
+ if (authenticationError) return { error: authenticationError };
897
947
  const error = await examErrorResult('exam_report', path, {
898
948
  status: response.status,
899
949
  json,
@@ -1317,7 +1367,7 @@ export function createTools(config) {
1317
1367
  inputSchema: {
1318
1368
  type: 'object', additionalProperties: false,
1319
1369
  properties: {
1320
- attemptId: { type: 'string', minLength: 1, description: 'Completed practice-run attempt id carrying immutable Exam capture.' },
1370
+ 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.' },
1321
1371
  shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Exact lowercase connected Shopify development-store domain.' },
1322
1372
  },
1323
1373
  required: ['attemptId', 'shopDomain'],
@@ -1329,7 +1379,7 @@ export function createTools(config) {
1329
1379
  inputSchema: {
1330
1380
  type: 'object', additionalProperties: false,
1331
1381
  properties: {
1332
- attemptId: { type: 'string', minLength: 1, description: 'Completed practice-run attempt id carrying immutable Exam capture.' },
1382
+ 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.' },
1333
1383
  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.' },
1334
1384
  },
1335
1385
  required: ['attemptId', 'shopDomain'],
@@ -1362,7 +1412,7 @@ export function createTools(config) {
1362
1412
  properties: {
1363
1413
  storeId: { type: 'string', minLength: 1, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
1364
1414
  worldId: { type: 'string', minLength: 1, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
1365
- 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.' },
1415
+ 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.' },
1366
1416
  clock: { type: 'object', description: 'Existing hosted practice-run clock contract.' },
1367
1417
  externalAgent: { type: 'object', description: 'Optional bounded external-agent identity metadata.' },
1368
1418
  subject: {
@@ -1388,7 +1438,7 @@ export function createTools(config) {
1388
1438
  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.',
1389
1439
  inputSchema: {
1390
1440
  type: 'object', additionalProperties: false,
1391
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1441
+ 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).' } },
1392
1442
  required: ['attemptId'],
1393
1443
  },
1394
1444
  },
@@ -1397,7 +1447,7 @@ export function createTools(config) {
1397
1447
  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.',
1398
1448
  inputSchema: {
1399
1449
  type: 'object', additionalProperties: false,
1400
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1450
+ 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).' } },
1401
1451
  required: ['attemptId'],
1402
1452
  },
1403
1453
  },
@@ -1408,7 +1458,7 @@ export function createTools(config) {
1408
1458
  type: 'object',
1409
1459
  additionalProperties: false,
1410
1460
  properties: {
1411
- attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
1461
+ 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).' },
1412
1462
  days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
1413
1463
  until: { type: 'object', description: 'Existing meguro.practice-run-until.v1 event-gated request.' },
1414
1464
  expectedDay: { type: 'integer', minimum: 0, description: 'Current practiceRun.time.watermarkDay.' },
@@ -1423,7 +1473,7 @@ export function createTools(config) {
1423
1473
  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.',
1424
1474
  inputSchema: {
1425
1475
  type: 'object', additionalProperties: false,
1426
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1476
+ 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).' } },
1427
1477
  required: ['attemptId'],
1428
1478
  },
1429
1479
  },
@@ -1432,7 +1482,7 @@ export function createTools(config) {
1432
1482
  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.',
1433
1483
  inputSchema: {
1434
1484
  type: 'object', additionalProperties: false,
1435
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1485
+ 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).' } },
1436
1486
  required: ['attemptId'],
1437
1487
  },
1438
1488
  },
@@ -1441,7 +1491,7 @@ export function createTools(config) {
1441
1491
  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.',
1442
1492
  inputSchema: {
1443
1493
  type: 'object', additionalProperties: false,
1444
- properties: { attemptId: { type: 'string', minLength: 1, description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
1494
+ 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).' } },
1445
1495
  required: ['attemptId'],
1446
1496
  },
1447
1497
  },
@@ -1494,9 +1544,25 @@ export function createTools(config) {
1494
1544
  const { title, ...annotations } = presentation;
1495
1545
  return { ...definition, title, annotations };
1496
1546
  });
1547
+ const definitionsByName = new Map(definitions.map((definition) => [definition.name, definition]));
1548
+
1549
+ function validateTopLevelArguments(name, args) {
1550
+ const definition = definitionsByName.get(name);
1551
+ if (!definition) return;
1552
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
1553
+ throw new Error(`${name} arguments must be an object`);
1554
+ }
1555
+ if (definition.inputSchema.additionalProperties !== false) return;
1556
+ const allowed = new Set(Object.keys(definition.inputSchema.properties ?? {}));
1557
+ const unexpected = Object.keys(args).filter((key) => !allowed.has(key)).sort();
1558
+ if (unexpected.length > 0) {
1559
+ throw new Error(`${name} arguments must match the declared top-level properties: ${[...allowed].join(', ') || '(none)'}`);
1560
+ }
1561
+ }
1497
1562
 
1498
1563
  async function call(name, args = {}) {
1499
1564
  try {
1565
+ validateTopLevelArguments(name, args);
1500
1566
  switch (name) {
1501
1567
  case 'docs_read': {
1502
1568
  const topic = requiredString(args, 'topic');
@@ -1778,13 +1844,13 @@ export function createTools(config) {
1778
1844
  )));
1779
1845
  }
1780
1846
  case 'exam_preflight': {
1781
- const attemptId = requiredString(args, 'attemptId');
1847
+ const attemptId = requiredPracticeAttemptId(args);
1782
1848
  const shopDomain = requiredShopifyDevDomain(args);
1783
1849
  const response = await examRequest(name, 'POST', '/practice-exams/preflight', { attemptId, shopDomain }, attemptId);
1784
1850
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
1785
1851
  }
1786
1852
  case 'exam_start': {
1787
- const attemptId = requiredString(args, 'attemptId');
1853
+ const attemptId = requiredPracticeAttemptId(args);
1788
1854
  const shopDomain = requiredShopifyDevDomain(args);
1789
1855
  const created = await examRequest(name, 'POST', '/practice-exams', { attemptId, shopDomain }, attemptId);
1790
1856
  if ('error' in created) return created.error;
@@ -1869,7 +1935,7 @@ export function createTools(config) {
1869
1935
  const storeId = requiredPracticeStoreId(args);
1870
1936
  const body = {
1871
1937
  purpose: 'external-agent',
1872
- ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: args.continueFromAttemptId } : {}),
1938
+ ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
1873
1939
  ...(args.clock !== undefined ? { clock: args.clock } : {}),
1874
1940
  ...(args.externalAgent !== undefined ? { externalAgent: args.externalAgent } : {}),
1875
1941
  ...(args.subject !== undefined ? { subject: args.subject } : {}),
@@ -1893,7 +1959,7 @@ export function createTools(config) {
1893
1959
  }));
1894
1960
  }
1895
1961
  case 'practice_run_status': {
1896
- const attemptId = requiredString(args, 'attemptId');
1962
+ const attemptId = requiredPracticeAttemptId(args);
1897
1963
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`);
1898
1964
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1899
1965
  const state = response.json ?? {};
@@ -1905,7 +1971,7 @@ export function createTools(config) {
1905
1971
  }));
1906
1972
  }
1907
1973
  case 'practice_run_checkpoint': {
1908
- const attemptId = requiredString(args, 'attemptId');
1974
+ const attemptId = requiredPracticeAttemptId(args);
1909
1975
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {});
1910
1976
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1911
1977
  return textResult(secretSafe({
@@ -1917,7 +1983,7 @@ export function createTools(config) {
1917
1983
  }));
1918
1984
  }
1919
1985
  case 'practice_run_advance': {
1920
- const attemptId = requiredString(args, 'attemptId');
1986
+ const attemptId = requiredPracticeAttemptId(args);
1921
1987
  const hasDays = args.days !== undefined;
1922
1988
  const hasUntil = args.until !== undefined;
1923
1989
  if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
@@ -1942,7 +2008,7 @@ export function createTools(config) {
1942
2008
  }));
1943
2009
  }
1944
2010
  case 'practice_run_finish': {
1945
- const attemptId = requiredString(args, 'attemptId');
2011
+ const attemptId = requiredPracticeAttemptId(args);
1946
2012
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/finish`, {});
1947
2013
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1948
2014
  return textResult(secretSafe({
@@ -1953,7 +2019,7 @@ export function createTools(config) {
1953
2019
  }));
1954
2020
  }
1955
2021
  case 'practice_run_report': {
1956
- const attemptId = requiredString(args, 'attemptId');
2022
+ const attemptId = requiredPracticeAttemptId(args);
1957
2023
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`);
1958
2024
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1959
2025
  return textResult({
@@ -1963,7 +2029,7 @@ export function createTools(config) {
1963
2029
  });
1964
2030
  }
1965
2031
  case 'practice_run_impact': {
1966
- const attemptId = requiredString(args, 'attemptId');
2032
+ const attemptId = requiredPracticeAttemptId(args);
1967
2033
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`);
1968
2034
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1969
2035
  // T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim
@@ -1999,12 +2065,14 @@ export function createTools(config) {
1999
2065
  // MEG-34: always ask the server to mint a durable Console reference for a successful probe.
2000
2066
  createConsoleReference: true,
2001
2067
  };
2002
- const { ok, json } = await apiRaw('POST', `/practice/${encodeURIComponent(worldId)}/probe/admin`, body);
2068
+ const { ok, status, json } = await apiRaw('POST', `/practice/${encodeURIComponent(worldId)}/probe/admin`, body);
2003
2069
  if (ok) {
2004
2070
  // Replace the internal reference id with a ready-to-open Console URL (opaque ref only).
2005
2071
  const { consoleRef, consoleRefExpiresAt, ...envelope } = json;
2006
2072
  return textResult({ ...envelope, ...consoleUrlFields(consoleRef, consoleRefExpiresAt) });
2007
2073
  }
2074
+ const authenticationError = authenticationErrorResult(status, json);
2075
+ if (authenticationError) return authenticationError;
2008
2076
  // Preserve the structured meguro.admin-probe.v1 error body (code + message) with isError: true,
2009
2077
  // instead of collapsing it to an unstructured thrown message.
2010
2078
  return { content: [{ type: 'text', text: JSON.stringify(secretSafe(json), null, 2) }], isError: true };
@@ -2023,6 +2091,7 @@ export function createTools(config) {
2023
2091
  return errorResult(`Unknown tool: ${name}`);
2024
2092
  }
2025
2093
  } catch (error) {
2094
+ if (error instanceof ToolResultError) return error.result;
2026
2095
  if (FLEET_TOOL_NAMES.has(name)) {
2027
2096
  return fleetErrorResult(name, `mcp:${name}`, 0, {
2028
2097
  errors: [{