meguro-mcp 0.2.1 → 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,15 @@
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
+
5
14
  ## 0.2.1 — 2026-08-01
6
15
 
7
16
  - 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.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.1
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.1"],
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.1",
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.1' };
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,7 @@ 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.',
@@ -281,6 +282,14 @@ function requiredPracticeStoreId(args) {
281
282
  return validatedStoreId(canonical, provided[0]);
282
283
  }
283
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
+
284
293
  function optionalPositiveInteger(args, key) {
285
294
  const value = args?.[key];
286
295
  if (value === undefined) return undefined;
@@ -1358,7 +1367,7 @@ export function createTools(config) {
1358
1367
  inputSchema: {
1359
1368
  type: 'object', additionalProperties: false,
1360
1369
  properties: {
1361
- 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.' },
1362
1371
  shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Exact lowercase connected Shopify development-store domain.' },
1363
1372
  },
1364
1373
  required: ['attemptId', 'shopDomain'],
@@ -1370,7 +1379,7 @@ export function createTools(config) {
1370
1379
  inputSchema: {
1371
1380
  type: 'object', additionalProperties: false,
1372
1381
  properties: {
1373
- 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.' },
1374
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.' },
1375
1384
  },
1376
1385
  required: ['attemptId', 'shopDomain'],
@@ -1403,7 +1412,7 @@ export function createTools(config) {
1403
1412
  properties: {
1404
1413
  storeId: { type: 'string', minLength: 1, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
1405
1414
  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.' },
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.' },
1407
1416
  clock: { type: 'object', description: 'Existing hosted practice-run clock contract.' },
1408
1417
  externalAgent: { type: 'object', description: 'Optional bounded external-agent identity metadata.' },
1409
1418
  subject: {
@@ -1429,7 +1438,7 @@ export function createTools(config) {
1429
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.',
1430
1439
  inputSchema: {
1431
1440
  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).' } },
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).' } },
1433
1442
  required: ['attemptId'],
1434
1443
  },
1435
1444
  },
@@ -1438,7 +1447,7 @@ export function createTools(config) {
1438
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.',
1439
1448
  inputSchema: {
1440
1449
  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).' } },
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).' } },
1442
1451
  required: ['attemptId'],
1443
1452
  },
1444
1453
  },
@@ -1449,7 +1458,7 @@ export function createTools(config) {
1449
1458
  type: 'object',
1450
1459
  additionalProperties: false,
1451
1460
  properties: {
1452
- 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).' },
1453
1462
  days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
1454
1463
  until: { type: 'object', description: 'Existing meguro.practice-run-until.v1 event-gated request.' },
1455
1464
  expectedDay: { type: 'integer', minimum: 0, description: 'Current practiceRun.time.watermarkDay.' },
@@ -1464,7 +1473,7 @@ export function createTools(config) {
1464
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.',
1465
1474
  inputSchema: {
1466
1475
  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).' } },
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).' } },
1468
1477
  required: ['attemptId'],
1469
1478
  },
1470
1479
  },
@@ -1473,7 +1482,7 @@ export function createTools(config) {
1473
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.',
1474
1483
  inputSchema: {
1475
1484
  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).' } },
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).' } },
1477
1486
  required: ['attemptId'],
1478
1487
  },
1479
1488
  },
@@ -1482,7 +1491,7 @@ export function createTools(config) {
1482
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.',
1483
1492
  inputSchema: {
1484
1493
  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).' } },
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).' } },
1486
1495
  required: ['attemptId'],
1487
1496
  },
1488
1497
  },
@@ -1535,9 +1544,25 @@ export function createTools(config) {
1535
1544
  const { title, ...annotations } = presentation;
1536
1545
  return { ...definition, title, annotations };
1537
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
+ }
1538
1562
 
1539
1563
  async function call(name, args = {}) {
1540
1564
  try {
1565
+ validateTopLevelArguments(name, args);
1541
1566
  switch (name) {
1542
1567
  case 'docs_read': {
1543
1568
  const topic = requiredString(args, 'topic');
@@ -1819,13 +1844,13 @@ export function createTools(config) {
1819
1844
  )));
1820
1845
  }
1821
1846
  case 'exam_preflight': {
1822
- const attemptId = requiredString(args, 'attemptId');
1847
+ const attemptId = requiredPracticeAttemptId(args);
1823
1848
  const shopDomain = requiredShopifyDevDomain(args);
1824
1849
  const response = await examRequest(name, 'POST', '/practice-exams/preflight', { attemptId, shopDomain }, attemptId);
1825
1850
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
1826
1851
  }
1827
1852
  case 'exam_start': {
1828
- const attemptId = requiredString(args, 'attemptId');
1853
+ const attemptId = requiredPracticeAttemptId(args);
1829
1854
  const shopDomain = requiredShopifyDevDomain(args);
1830
1855
  const created = await examRequest(name, 'POST', '/practice-exams', { attemptId, shopDomain }, attemptId);
1831
1856
  if ('error' in created) return created.error;
@@ -1910,7 +1935,7 @@ export function createTools(config) {
1910
1935
  const storeId = requiredPracticeStoreId(args);
1911
1936
  const body = {
1912
1937
  purpose: 'external-agent',
1913
- ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: args.continueFromAttemptId } : {}),
1938
+ ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
1914
1939
  ...(args.clock !== undefined ? { clock: args.clock } : {}),
1915
1940
  ...(args.externalAgent !== undefined ? { externalAgent: args.externalAgent } : {}),
1916
1941
  ...(args.subject !== undefined ? { subject: args.subject } : {}),
@@ -1934,7 +1959,7 @@ export function createTools(config) {
1934
1959
  }));
1935
1960
  }
1936
1961
  case 'practice_run_status': {
1937
- const attemptId = requiredString(args, 'attemptId');
1962
+ const attemptId = requiredPracticeAttemptId(args);
1938
1963
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`);
1939
1964
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1940
1965
  const state = response.json ?? {};
@@ -1946,7 +1971,7 @@ export function createTools(config) {
1946
1971
  }));
1947
1972
  }
1948
1973
  case 'practice_run_checkpoint': {
1949
- const attemptId = requiredString(args, 'attemptId');
1974
+ const attemptId = requiredPracticeAttemptId(args);
1950
1975
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {});
1951
1976
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1952
1977
  return textResult(secretSafe({
@@ -1958,7 +1983,7 @@ export function createTools(config) {
1958
1983
  }));
1959
1984
  }
1960
1985
  case 'practice_run_advance': {
1961
- const attemptId = requiredString(args, 'attemptId');
1986
+ const attemptId = requiredPracticeAttemptId(args);
1962
1987
  const hasDays = args.days !== undefined;
1963
1988
  const hasUntil = args.until !== undefined;
1964
1989
  if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
@@ -1983,7 +2008,7 @@ export function createTools(config) {
1983
2008
  }));
1984
2009
  }
1985
2010
  case 'practice_run_finish': {
1986
- const attemptId = requiredString(args, 'attemptId');
2011
+ const attemptId = requiredPracticeAttemptId(args);
1987
2012
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/finish`, {});
1988
2013
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
1989
2014
  return textResult(secretSafe({
@@ -1994,7 +2019,7 @@ export function createTools(config) {
1994
2019
  }));
1995
2020
  }
1996
2021
  case 'practice_run_report': {
1997
- const attemptId = requiredString(args, 'attemptId');
2022
+ const attemptId = requiredPracticeAttemptId(args);
1998
2023
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`);
1999
2024
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
2000
2025
  return textResult({
@@ -2004,7 +2029,7 @@ export function createTools(config) {
2004
2029
  });
2005
2030
  }
2006
2031
  case 'practice_run_impact': {
2007
- const attemptId = requiredString(args, 'attemptId');
2032
+ const attemptId = requiredPracticeAttemptId(args);
2008
2033
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`);
2009
2034
  if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
2010
2035
  // T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim