meguro-mcp 0.2.7 → 0.2.8

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.8 — 2026-08-09
6
+
7
+ - Adds `practice_runs_list` so a cold MCP-only evaluator can discover existing practice attempts
8
+ without guessing a store or run identifier.
9
+ - Exposes `admin_schema` through the hosted-safe registry, corrects hosted OAuth connection
10
+ guidance, and adds immutable documentation currency/index metadata.
11
+ - Changes no earlier immutable documentation byte stream and carries no unrelated tool or protocol
12
+ behavior beyond the landed MEG-768 cold-walk corrections.
13
+
5
14
  ## 0.2.7 — 2026-08-08
6
15
 
7
16
  - Adds the optional `storeId` selector to `gate_evaluate` and `gate_verdict`, preserving every
package/README.md CHANGED
@@ -43,7 +43,7 @@ Console provides fast trusted proof and evidence inspection; the agency agent no
43
43
 
44
44
  <!-- BEGIN GENERATED MCP TOOL TABLE -->
45
45
  <!-- Run `npm run docs:tools --workspace meguro-mcp` to refresh this section from `tools/list`. -->
46
- This table is generated from the server's live `tools/list` response (46 tools).
46
+ This table is generated from the server's live `tools/list` response (47 tools).
47
47
 
48
48
  | Tool | Title | Behavior annotations |
49
49
  |---|---|---|
@@ -84,6 +84,7 @@ This table is generated from the server's live `tools/list` response (46 tools).
84
84
  | `exam_status` | Read Shopify Exam status | read-only · non-destructive · idempotent · closed-world |
85
85
  | `exam_report` | Read a Shopify Exam receipt | read-only · non-destructive · idempotent · closed-world |
86
86
  | `practice_run_start` | Start a practice run | read-write · non-destructive · non-idempotent · closed-world |
87
+ | `practice_runs_list` | List practice runs | read-only · non-destructive · idempotent · closed-world |
87
88
  | `practice_run_status` | Read practice-run status | read-only · non-destructive · idempotent · closed-world |
88
89
  | `practice_run_checkpoint` | Capture a practice-run checkpoint | read-write · non-destructive · non-idempotent · closed-world |
89
90
  | `practice_run_advance` | Advance store time | read-write · non-destructive · non-idempotent · closed-world |
@@ -185,7 +186,7 @@ OAuth support. It requires a workspace-bound `meg_sk_…` API key and exposes th
185
186
  surface. Pin the exact public version in client configuration so a quickstart stays reproducible:
186
187
 
187
188
  ```bash
188
- npx -y meguro-mcp@0.2.7
189
+ npx -y meguro-mcp@0.2.8
189
190
  ```
190
191
 
191
192
  ### Environment
@@ -203,7 +204,7 @@ claude mcp add meguro \
203
204
  -e MEGURO_API_BASE_URL=https://api-dev.meguro.io \
204
205
  -e MEGURO_API_TOKEN=meg_sk_... \
205
206
  -e MEGURO_DASHBOARD_URL=https://... \
206
- -- npx -y meguro-mcp@0.2.7
207
+ -- npx -y meguro-mcp@0.2.8
207
208
  ```
208
209
 
209
210
  ### Register — Cursor (`.cursor/mcp.json`)
@@ -213,7 +214,7 @@ claude mcp add meguro \
213
214
  "mcpServers": {
214
215
  "meguro": {
215
216
  "command": "npx",
216
- "args": ["-y", "meguro-mcp@0.2.7"],
217
+ "args": ["-y", "meguro-mcp@0.2.8"],
217
218
  "env": {
218
219
  "MEGURO_API_BASE_URL": "https://api-dev.meguro.io",
219
220
  "MEGURO_API_TOKEN": "meg_sk_...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meguro-mcp",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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/docs.mjs CHANGED
@@ -41,8 +41,45 @@ const GETTING_STARTED_V6_URI = `meguro://docs/getting-started/v${GETTING_STARTED
41
41
  const PRODUCT_GUIDE_URI = `meguro://docs/product-guide/v${PRODUCT_GUIDE_VERSION}`;
42
42
  const PRODUCT_GUIDE_V2_URI = `meguro://docs/product-guide/v${PRODUCT_GUIDE_V2_VERSION}`;
43
43
  const PRODUCT_GUIDE_V3_URI = `meguro://docs/product-guide/v${PRODUCT_GUIDE_V3_VERSION}`;
44
+ export const DOCUMENTATION_INDEX_URI = 'meguro://docs/index/v1';
45
+
46
+ const DOCUMENTATION_CURRENCY_V1 = deepFreeze({
47
+ indexVersion: 1,
48
+ currentUriByTopic: {
49
+ 'gate-policy': GATE_POLICY_V2_URI,
50
+ 'getting-started': GETTING_STARTED_V6_URI,
51
+ index: DOCUMENTATION_INDEX_URI,
52
+ 'product-guide': PRODUCT_GUIDE_V3_URI,
53
+ 'receipt-guide': RECEIPT_GUIDE_V7_URI,
54
+ },
55
+ });
56
+ const CURRENT_DOCUMENTATION_CURRENCY = DOCUMENTATION_CURRENCY_V1;
57
+
58
+ function documentationVersionFromUri(uri) {
59
+ const match = /\/v([1-9][0-9]*)$/u.exec(uri);
60
+ if (!match) throw new Error(`Documentation URI has no immutable version: ${uri}`);
61
+ return Number(match[1]);
62
+ }
44
63
 
45
- export const MCP_INITIALIZE_INSTRUCTIONS = `Start with \`${GETTING_STARTED_V6_URI}\` or call \`docs_read({ topic: "getting-started", version: 6 })\`; for general Meguro questions, read \`${PRODUCT_GUIDE_V3_URI}\` or call \`docs_read({ topic: "product-guide", version: 3 })\`.`;
64
+ function documentationIndexText(currency) {
65
+ const rows = Object.entries(currency.currentUriByTopic).sort(([left], [right]) => codepointCompare(left, right));
66
+ return [
67
+ `# Meguro documentation index v${currency.indexVersion}`,
68
+ '',
69
+ `Published URI: \`${DOCUMENTATION_INDEX_URI}\``,
70
+ '',
71
+ 'This immutable index records the current published version of every documentation family at issuance.',
72
+ 'Use the `meguro/status` annotations from `resources/list` for live currency; cited versioned resources never change.',
73
+ '',
74
+ '| Family | Current version | Current resource |',
75
+ '| --- | ---: | --- |',
76
+ ...rows.map(([topic, uri]) => `| ${topic} | v${documentationVersionFromUri(uri)} | ${uri} |`),
77
+ ].join('\n');
78
+ }
79
+
80
+ const CURRENT_GETTING_STARTED_URI = CURRENT_DOCUMENTATION_CURRENCY.currentUriByTopic['getting-started'];
81
+ const CURRENT_PRODUCT_GUIDE_URI = CURRENT_DOCUMENTATION_CURRENCY.currentUriByTopic['product-guide'];
82
+ export const MCP_INITIALIZE_INSTRUCTIONS = `Start with \`${CURRENT_GETTING_STARTED_URI}\` or call \`docs_read({ topic: "getting-started", version: ${documentationVersionFromUri(CURRENT_GETTING_STARTED_URI)} })\`; for general Meguro questions, read \`${CURRENT_PRODUCT_GUIDE_URI}\` or call \`docs_read({ topic: "product-guide", version: ${documentationVersionFromUri(CURRENT_PRODUCT_GUIDE_URI)} })\`.`;
46
83
  export const PRODUCT_GUIDE_SHA256 = 'e5450c1b4181cec4d6ed0a498dbff1cb25d2e223ff2228928aecbbce9674f074';
47
84
 
48
85
  function deepFreeze(value) {
@@ -1262,13 +1299,13 @@ function entry(input) {
1262
1299
  name: input.name,
1263
1300
  title: input.title,
1264
1301
  description: input.description,
1265
- mimeType: 'text/markdown',
1302
+ mimeType: input.mimeType ?? 'text/markdown',
1266
1303
  text,
1267
1304
  sha256: digest,
1268
1305
  });
1269
1306
  }
1270
1307
 
1271
- export function buildDocumentationCatalog(inputs) {
1308
+ export function buildDocumentationCatalog(inputs, currency) {
1272
1309
  const rows = inputs.map(entry).sort((left, right) => codepointCompare(left.uri, right.uri));
1273
1310
  const byUri = new Map();
1274
1311
  const byTopicVersion = new Map();
@@ -1279,6 +1316,19 @@ export function buildDocumentationCatalog(inputs) {
1279
1316
  byUri.set(row.uri, row);
1280
1317
  byTopicVersion.set(key, row);
1281
1318
  }
1319
+ const currentUriByTopic = currency?.currentUriByTopic ?? Object.fromEntries(
1320
+ [...new Set(rows.map((row) => row.topic))].map((topic) => {
1321
+ const current = rows.filter((row) => row.topic === topic).sort((left, right) => left.version - right.version).at(-1);
1322
+ return [topic, current.uri];
1323
+ }),
1324
+ );
1325
+ for (const topic of new Set(rows.map((row) => row.topic))) {
1326
+ const currentUri = currentUriByTopic[topic];
1327
+ const current = byUri.get(currentUri);
1328
+ if (!current || current.topic !== topic) {
1329
+ throw new Error(`Documentation currency has no valid current resource for ${topic}`);
1330
+ }
1331
+ }
1282
1332
  return deepFreeze({
1283
1333
  list() {
1284
1334
  return rows.map((row) => deepFreeze({
@@ -1292,6 +1342,8 @@ export function buildDocumentationCatalog(inputs) {
1292
1342
  'meguro/topic': row.topic,
1293
1343
  'meguro/version': row.version,
1294
1344
  'meguro/sha256': row.sha256,
1345
+ 'meguro/status': row.uri === currentUriByTopic[row.topic] ? 'current' : 'superseded',
1346
+ 'meguro/current-uri': currentUriByTopic[row.topic],
1295
1347
  },
1296
1348
  }));
1297
1349
  },
@@ -1391,6 +1443,15 @@ const CATALOG = buildDocumentationCatalog([
1391
1443
  description: 'World-first product concepts, evidence boundaries, surface responsibilities, and question-to-source routing.',
1392
1444
  text: PRODUCT_GUIDE_V3_TEXT,
1393
1445
  },
1446
+ {
1447
+ topic: 'index',
1448
+ version: DOCUMENTATION_CURRENCY_V1.indexVersion,
1449
+ uri: DOCUMENTATION_INDEX_URI,
1450
+ name: 'Meguro documentation index v1',
1451
+ title: 'Meguro documentation index v1',
1452
+ description: 'Immutable current-version pointers for every published Meguro documentation family.',
1453
+ text: documentationIndexText(DOCUMENTATION_CURRENCY_V1),
1454
+ },
1394
1455
  {
1395
1456
  topic: 'receipt-guide',
1396
1457
  version: RECEIPT_GUIDE_VERSION,
@@ -1472,7 +1533,7 @@ const CATALOG = buildDocumentationCatalog([
1472
1533
  description: 'Immutable named checks and flip conditions republished with the simulation-run vocabulary.',
1473
1534
  text: GATE_POLICY_V2_TEXT,
1474
1535
  },
1475
- ]);
1536
+ ], CURRENT_DOCUMENTATION_CURRENCY);
1476
1537
 
1477
1538
  export function documentationResources() {
1478
1539
  return CATALOG.list();
package/src/server.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from './protocol.mjs';
15
15
  import { createTools, redactSecrets } from './tools.mjs';
16
16
 
17
- const SERVER_INFO = { name: 'meguro', version: '0.2.7' };
17
+ const SERVER_INFO = { name: 'meguro', version: '0.2.8' };
18
18
  const PROTOCOL_VERSION = '2025-03-26';
19
19
 
20
20
  const tools = createTools({
package/src/tools.mjs CHANGED
@@ -26,6 +26,7 @@ const DOCUMENTATION_TOOL_CONTRACT = documentationToolContract();
26
26
  const DOCUMENTATION_TOPICS = new Set(DOCUMENTATION_TOOL_CONTRACT.topics);
27
27
  const FLEET_TOOL_NAMES = new Set([
28
28
  'templates_list', 'stores_list', 'store_create', 'store_delete', 'store_passport',
29
+ 'practice_runs_list',
29
30
  'workspaces_list', 'workspace_create', 'workspace_archive', 'workspace_unarchive',
30
31
  'share_create', 'shares_list', 'share_status', 'share_publish', 'share_revoke',
31
32
  'catalog_slice_read', 'catalog_slice_snapshot', 'catalog_slices_saved',
@@ -69,6 +70,7 @@ const TOOL_PRESENTATION = Object.freeze({
69
70
  exam_status: { title: 'Read Shopify Exam status', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
70
71
  exam_report: { title: 'Read a Shopify Exam receipt', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
71
72
  practice_run_start: { title: 'Start a practice run', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
73
+ practice_runs_list: { title: 'List practice runs', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
72
74
  practice_run_status: { title: 'Read practice-run status', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
73
75
  practice_run_checkpoint: { title: 'Capture a practice-run checkpoint', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
74
76
  practice_run_advance: { title: 'Advance store time', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
@@ -832,13 +834,94 @@ function runsListProjection(value, lifecycle) {
832
834
  ...(visible.length === 0 ? {
833
835
  emptyState: {
834
836
  message: `No ${lifecycle ?? 'matching'} Shopify dev-store history runs were found. This says nothing about practice simulation attempts.`,
835
- practiceAttemptRecovery: 'For a known practice simulation attemptId, call practice_run_report and practice_run_impact. To discover an attemptId, open the Console Runs/Receipts view; MCP has no practice-attempt list tool today.',
837
+ practiceAttemptRecovery: 'Call practice_runs_list to discover practice simulation attempts, then pass one exact attemptId to practice_run_report or practice_run_impact.',
836
838
  },
837
839
  } : {}),
838
840
  clockDiscipline: 'runClock names lifecycle timestamps; worldClock names scenario coordinates and does not invent an unreported Store-time.',
839
841
  });
840
842
  }
841
843
 
844
+ function practiceRunsSelectionError(storeIds) {
845
+ const multiple = storeIds.length > 1;
846
+ const meaning = multiple
847
+ ? `This workspace has more than one practice store (${storeIds.length}), so Meguro cannot infer which store's practice runs to list`
848
+ : 'This workspace has no practice store, so there are no store-owned practice runs to list';
849
+ const nextStep = multiple
850
+ ? 'Call stores_list({}), choose one exact storeId, then call practice_runs_list({ storeId: "<exact storeId>" }). You may instead provide a known receiptId so Meguro can derive its owning store.'
851
+ : 'Create a practice store first, then call practice_runs_list with its storeId or with a receiptId from one of its runs.';
852
+ return {
853
+ content: [{
854
+ type: 'text',
855
+ text: JSON.stringify({
856
+ code: 'store-selection-required',
857
+ storeIds,
858
+ errors: [{ code: 'store-selection-required', message: `Store selection required. ${meaning}. ${nextStep}`, meaning, nextStep }],
859
+ }, null, 2),
860
+ }],
861
+ isError: true,
862
+ };
863
+ }
864
+
865
+ function practiceRunReceiptSelectionError(receiptId) {
866
+ return {
867
+ content: [{
868
+ type: 'text',
869
+ text: JSON.stringify({
870
+ code: 'receipt-unavailable',
871
+ errors: [{
872
+ code: 'receipt-unavailable',
873
+ message: `Practice receipt ${receiptId} is not available in this workspace.`,
874
+ meaning: 'The receipt does not identify a retained practice run owned by this workspace.',
875
+ nextStep: 'Call practice_runs_list with an exact current storeId, or use a receiptId returned by a prior practice_runs_list or practice_run_report response.',
876
+ }],
877
+ }, null, 2),
878
+ }],
879
+ isError: true,
880
+ };
881
+ }
882
+
883
+ function practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedStoreId) {
884
+ return {
885
+ content: [{
886
+ type: 'text',
887
+ text: JSON.stringify({
888
+ code: 'receipt-store-mismatch',
889
+ errors: [{
890
+ code: 'receipt-store-mismatch',
891
+ message: `Receipt and storeId identify different practice stores. Receipt ${receiptId} belongs to ${receiptStoreId}, not ${requestedStoreId}.`,
892
+ meaning: `Receipt ${receiptId} belongs to store ${receiptStoreId}, not ${requestedStoreId}.`,
893
+ nextStep: `Retry with storeId ${receiptStoreId}, or omit storeId and let Meguro derive it from receiptId ${receiptId}.`,
894
+ }],
895
+ }, null, 2),
896
+ }],
897
+ isError: true,
898
+ };
899
+ }
900
+
901
+ function practiceRunsProjection(value, storeId, storeSelection) {
902
+ const attempts = Array.isArray(value?.playbacks) ? value.playbacks : [];
903
+ return secretSafe({
904
+ schemaVersion: 'meguro.practice-run-list.v1',
905
+ storeId,
906
+ storeSelection,
907
+ attempts: attempts
908
+ .filter((attempt) => attempt?.storeId === storeId || attempt?.worldId === storeId)
909
+ .map((attempt) => ({
910
+ attemptId: attempt.attemptId,
911
+ state: attempt.status,
912
+ clock: {
913
+ mode: attempt.clock?.mode ?? null,
914
+ baselineDay: attempt.baselineDay ?? null,
915
+ watermarkDay: attempt.watermarkDay ?? null,
916
+ simulationDays: attempt.simulationDays ?? attempt.clock?.simulationDays ?? null,
917
+ elapsedSimulationDays: attempt.elapsedSimulationDays ?? null,
918
+ remainingSimulationDays: attempt.remainingSimulationDays ?? null,
919
+ },
920
+ receiptRef: { attemptId: attempt.attemptId },
921
+ })),
922
+ });
923
+ }
924
+
842
925
  function practiceAttemptComparisonGuidanceFor(toolName, a, b) {
843
926
  const attemptIds = [a, b].filter((id) => PRACTICE_ATTEMPT_ID.test(id));
844
927
  const calls = attemptIds.flatMap((attemptId) => [
@@ -900,6 +983,21 @@ function practiceStoreConnectionNotFoundGuidance(storeId) {
900
983
  };
901
984
  }
902
985
 
986
+ function practiceRunsStoreNotFoundGuidance(storeId) {
987
+ return {
988
+ content: [{
989
+ type: 'text',
990
+ text: JSON.stringify({
991
+ code: 'practice-store-not-found',
992
+ message: `practice_runs_list could not find practice store ${storeId} in this account or the store is no longer active.`,
993
+ nextAction: 'Call stores_list({}) and use an exact current storeId from the returned stores array.',
994
+ stopCondition: `If stores_list({}) returns no current stores, stop: there is no practice store whose attempts can be listed. Do not retry ${JSON.stringify(storeId)}.`,
995
+ }, null, 2),
996
+ }],
997
+ isError: true,
998
+ };
999
+ }
1000
+
903
1001
  function legacyGateArtifactNotFoundGuidance(runId) {
904
1002
  return {
905
1003
  content: [{
@@ -1881,7 +1979,7 @@ export function createTools(config) {
1881
1979
  },
1882
1980
  {
1883
1981
  name: 'runs_list',
1884
- description: 'List Shopify dev-store history runs created by run_start so an agent can re-find that family of prior work. It does not list practice simulation attempts returned by practice_run_start; those use pa-* ids, practice_run_report, and practice_run_impact, while attempt discovery lives in the Console Runs/Receipts view today. Optional history-run lifecycle filtering supports running, paused, completed, failed, or cleaning. An empty result explicitly says it is only about history runs. The payload names run-clock lifecycle timestamps separately from world-clock scenario coordinates and returns no filter counts.',
1982
+ description: 'List Shopify dev-store history runs created by run_start so an agent can re-find that family of prior work. It does not list practice simulation attempts returned by practice_run_start; call practice_runs_list for those pa-* ids, then use practice_run_report or practice_run_impact. Optional history-run lifecycle filtering supports running, paused, completed, failed, or cleaning. An empty result explicitly says it is only about history runs. The payload names run-clock lifecycle timestamps separately from world-clock scenario coordinates and returns no filter counts.',
1885
1983
  inputSchema: {
1886
1984
  type: 'object',
1887
1985
  additionalProperties: false,
@@ -1992,6 +2090,20 @@ export function createTools(config) {
1992
2090
  anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
1993
2091
  },
1994
2092
  },
2093
+ {
2094
+ name: 'practice_runs_list',
2095
+ description: 'List bounded practice simulation attempt facts for one exact practice store: attemptId, lifecycle state, Store-clock summary, and a receiptRef that can be passed unchanged to practice_run_report. Store selection follows one fail-closed ladder: receiptId derives its owning store; otherwise an explicit storeId wins; omission is allowed only when the workspace has exactly one current store; zero or multiple stores return a structured store-selection-required error and never choose silently. An OAuth grant may select an owned workspace; an account API key remains bound to its own workspace.',
2096
+ inputSchema: {
2097
+ type: 'object',
2098
+ additionalProperties: false,
2099
+ properties: {
2100
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional OAuth selector for an account-owned non-default workspace. An account API key can name only its bound workspace.' },
2101
+ storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Optional exact practice-store id from stores_list. Omit only with receiptId or when the workspace has exactly one current store.' },
2102
+ receiptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Optional known pa-* practice receipt identity. Meguro derives its owning store and refuses a conflicting storeId.' },
2103
+ },
2104
+ required: [],
2105
+ },
2106
+ },
1995
2107
  {
1996
2108
  name: 'practice_run_status',
1997
2109
  description: 'Read the server-authoritative state of an external-agent practice simulation run, including its announced simulation-run deadlines and immutable simulation run receipt after any exit. Use Console for quick proof; the tested agent remains in the caller\'s environment.',
@@ -2059,7 +2171,7 @@ export function createTools(config) {
2059
2171
  },
2060
2172
  {
2061
2173
  name: 'get_connection_details',
2062
- description: `Fetch connection material for supported Shopify Admin GraphQL versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns adminApiVersion (default), adminApiVersions (supported set), adminVersions (exact versioned URLs), the default URL, access token, and shop domain for an existing Meguro practice store so an assistant can configure a user agent without opening the dashboard. Inspect the selected version with admin_schema before the first Admin call; an agent targeting a newer Shopify release outside the supported set must treat that behavior as not established. Takes the canonical practice-store id (storeId) and returns it back, so the result passes directly into practice_run_start({ storeId }) — no identifier translation. The practice-store id is not an attemptId (run identity). Requires an account API key because it reveals the per-store token.`,
2174
+ description: `Fetch connection material for supported Shopify Admin GraphQL versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns adminApiVersion (default), adminApiVersions (supported set), adminVersions (exact versioned URLs), the default URL, access token, and shop domain for an existing Meguro practice store so an assistant can configure a user agent without opening the dashboard. Inspect the selected version with admin_schema before the first Admin call; an agent targeting a newer Shopify release outside the supported set must treat that behavior as not established. Takes the canonical practice-store id (storeId) and returns it back, so the result passes directly into practice_run_start({ storeId }) — no identifier translation. The practice-store id is not an attemptId (run identity). The hosted MCP path uses the caller's OAuth grant; the public STDIO path uses its workspace-bound account API key.`,
2063
2175
  inputSchema: {
2064
2176
  type: 'object',
2065
2177
  additionalProperties: false,
@@ -2630,6 +2742,50 @@ export function createTools(config) {
2630
2742
  ...consoleUrlFields(started.consoleRef, started.consoleRefExpiresAt),
2631
2743
  }));
2632
2744
  }
2745
+ case 'practice_runs_list': {
2746
+ const workspaceId = optionalWorkspaceId(args);
2747
+ const requestedStoreId = args.storeId === undefined
2748
+ ? undefined
2749
+ : validatedStoreId(requiredString(args, 'storeId'), 'storeId');
2750
+ const receiptId = args.receiptId === undefined
2751
+ ? undefined
2752
+ : requiredPracticeAttemptId(args, 'receiptId');
2753
+ if (receiptId) {
2754
+ const listed = await fleetRequest(name, 'GET', '/practice/playbacks', undefined, { workspaceId });
2755
+ if ('error' in listed) return listed.error;
2756
+ const playbacks = Array.isArray(listed.value?.playbacks) ? listed.value.playbacks : [];
2757
+ const referenced = playbacks.find((attempt) => attempt?.attemptId === receiptId);
2758
+ if (!referenced) return practiceRunReceiptSelectionError(receiptId);
2759
+ const receiptStoreId = String(referenced.storeId ?? referenced.worldId ?? '');
2760
+ if (requestedStoreId && requestedStoreId !== receiptStoreId) {
2761
+ return practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedStoreId);
2762
+ }
2763
+ return textResult(practiceRunsProjection(listed.value, receiptStoreId, 'receipt-derived'));
2764
+ }
2765
+
2766
+ const fleet = await fleetRequest(name, 'GET', '/practice/stores', undefined, { workspaceId });
2767
+ if ('error' in fleet) return fleet.error;
2768
+ const storeIds = [...new Set((Array.isArray(fleet.value?.stores) ? fleet.value.stores : [])
2769
+ .map((store) => String(store?.storeId ?? store?.worldId ?? '').trim())
2770
+ .filter((storeId) => /^[a-z0-9-]+$/u.test(storeId)))].sort();
2771
+
2772
+ if (requestedStoreId && !storeIds.includes(requestedStoreId)) {
2773
+ return practiceRunsStoreNotFoundGuidance(requestedStoreId);
2774
+ }
2775
+ if (!requestedStoreId && storeIds.length !== 1) {
2776
+ return practiceRunsSelectionError(storeIds);
2777
+ }
2778
+
2779
+ const listPath = queryPath('/practice/playbacks', { worldId: requestedStoreId ?? storeIds[0] });
2780
+ const listed = await fleetRequest(name, 'GET', listPath, undefined, { workspaceId });
2781
+ if ('error' in listed) return listed.error;
2782
+ const selectedStoreId = requestedStoreId ?? storeIds[0];
2783
+ return textResult(practiceRunsProjection(
2784
+ listed.value,
2785
+ selectedStoreId,
2786
+ requestedStoreId ? 'explicit-store-id' : 'single-store',
2787
+ ));
2788
+ }
2633
2789
  case 'practice_run_status': {
2634
2790
  const attemptId = requiredPracticeAttemptId(args);
2635
2791
  const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`);