@fluentcommerce/fluent-mcp-extn 0.2.0 → 0.3.0

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.
@@ -286,6 +286,7 @@ const ENTITY_REGISTRY = {
286
286
  'context is a plain String ("RETAILER"), NOT { contextType: "RETAILER" }',
287
287
  "contextId is a separate Int field, not combined with context",
288
288
  "For large JSON values, use lobValue instead of value (lobType: LOB)",
289
+ 'For Mystique manifest settings (fc.mystique.*), set valueType:"JSON" — LOB type breaks manifest parsing silently',
289
290
  ],
290
291
  hasRef: false,
291
292
  retailerScoped: false,
@@ -140,6 +140,36 @@ export class FluentClientAdapter {
140
140
  return FluentClientAdapter.unwrapResponse(response);
141
141
  });
142
142
  }
143
+ /**
144
+ * Fetch a workflow definition by name from the REST API.
145
+ * GET /api/v4.1/workflow/{retailerId}/{entityType}::{entitySubtype}/
146
+ * Optionally fetch a specific version:
147
+ * GET /api/v4.1/workflow/{retailerId}/{entityType}::{entitySubtype}/{version}
148
+ * Read-only — safe to retry.
149
+ */
150
+ async getWorkflow(retailerId, entityType, entitySubtype, version) {
151
+ const requestClient = this.requireRequestClient("Workflow REST API");
152
+ const workflowName = `${entityType}::${entitySubtype}`;
153
+ const path = version
154
+ ? `/api/v4.1/workflow/${retailerId}/${workflowName}/${version}`
155
+ : `/api/v4.1/workflow/${retailerId}/${workflowName}/`;
156
+ return this.execute("getWorkflow", async () => {
157
+ const response = await requestClient.request(path, { method: "GET" });
158
+ return FluentClientAdapter.unwrapResponse(response);
159
+ });
160
+ }
161
+ /**
162
+ * List all workflows for a retailer from the REST API.
163
+ * GET /api/v4.1/workflow?retailerId={retailerId}
164
+ * Read-only — safe to retry.
165
+ */
166
+ async listWorkflows(retailerId) {
167
+ const requestClient = this.requireRequestClient("Workflow REST API");
168
+ return this.execute("listWorkflows", async () => {
169
+ const response = await requestClient.request(`/api/v4.1/workflow?retailerId=${retailerId}&count=200`, { method: "GET" });
170
+ return FluentClientAdapter.unwrapResponse(response);
171
+ });
172
+ }
143
173
  /**
144
174
  * Query Prometheus metrics via GraphQL metricInstant / metricRange queries.
145
175
  * The Fluent platform does NOT expose raw Prometheus REST endpoints
@@ -149,19 +179,19 @@ export class FluentClientAdapter {
149
179
  async queryPrometheus(payload) {
150
180
  const isRange = payload.type === "range";
151
181
  const query = isRange
152
- ? `query MetricRange($query: String!, $start: DateTime!, $end: DateTime!, $step: String!) {
153
- metricRange(query: $query, start: $start, end: $end, step: $step) {
154
- status
155
- data { resultType result { metric values } }
156
- errorType error warnings
157
- }
182
+ ? `query MetricRange($query: String!, $start: DateTime!, $end: DateTime!, $step: String!) {
183
+ metricRange(query: $query, start: $start, end: $end, step: $step) {
184
+ status
185
+ data { resultType result { metric values } }
186
+ errorType error warnings
187
+ }
158
188
  }`
159
- : `query MetricInstant($query: String!${payload.time ? ", $time: DateTime" : ""}) {
160
- metricInstant(query: $query${payload.time ? ", time: $time" : ""}) {
161
- status
162
- data { resultType result { metric value } }
163
- errorType error warnings
164
- }
189
+ : `query MetricInstant($query: String!${payload.time ? ", $time: DateTime" : ""}) {
190
+ metricInstant(query: $query${payload.time ? ", time: $time" : ""}) {
191
+ status
192
+ data { resultType result { metric value } }
193
+ errorType error warnings
194
+ }
165
195
  }`;
166
196
  const variables = { query: payload.query };
167
197
  if (isRange) {
@@ -5,6 +5,8 @@
5
5
  * settings that workflows depend on (webhook URLs, feature flags, thresholds).
6
6
  */
7
7
  import { z } from "zod";
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
8
10
  import { ToolError } from "./errors.js";
9
11
  // ---------------------------------------------------------------------------
10
12
  // Input schemas
@@ -16,6 +18,11 @@ const SettingInputSchema = z.object({
16
18
  .string()
17
19
  .optional()
18
20
  .describe("Large object value (for JSON payloads > 4KB). Mutually exclusive with value."),
21
+ valueType: z
22
+ .enum(["STRING", "LOB", "JSON"])
23
+ .optional()
24
+ .describe('Explicit value type override. Use "JSON" for Mystique manifest settings (fc.mystique.*). ' +
25
+ 'Defaults to "LOB" when lobValue is provided, "STRING" when value is provided.'),
19
26
  context: z
20
27
  .string()
21
28
  .min(1)
@@ -26,6 +33,34 @@ const SettingInputSchema = z.object({
26
33
  .optional()
27
34
  .describe("Context ID (e.g., retailer ID). Falls back to FLUENT_RETAILER_ID for RETAILER context."),
28
35
  });
36
+ export const SettingGetInputSchema = z.object({
37
+ name: z
38
+ .string()
39
+ .min(1)
40
+ .describe('Setting name or pattern. Supports "%" wildcards (e.g., "fc.mystique.manifest.%" for all manifest settings).'),
41
+ context: z
42
+ .string()
43
+ .optional()
44
+ .describe('Filter by context: "ACCOUNT", "RETAILER". Omit for all contexts.'),
45
+ contextId: z
46
+ .number()
47
+ .int()
48
+ .optional()
49
+ .describe("Context ID. Falls back to FLUENT_RETAILER_ID for RETAILER context, 0 for ACCOUNT."),
50
+ outputFile: z
51
+ .string()
52
+ .optional()
53
+ .describe("Optional file path to save the setting value/lobValue. " +
54
+ "When provided, writes content to file and returns only metadata (name, context, valueType, size) — " +
55
+ "keeps large manifests/JSON out of the LLM context. Parent directories are created automatically."),
56
+ first: z
57
+ .number()
58
+ .int()
59
+ .min(1)
60
+ .max(100)
61
+ .default(10)
62
+ .describe("Max results to return (default: 10, max: 100)."),
63
+ });
29
64
  export const SettingUpsertInputSchema = SettingInputSchema;
30
65
  export const SettingBulkUpsertInputSchema = z.object({
31
66
  settings: z
@@ -38,6 +73,57 @@ export const SettingBulkUpsertInputSchema = z.object({
38
73
  // Tool definitions (JSON Schema for MCP)
39
74
  // ---------------------------------------------------------------------------
40
75
  export const SETTING_TOOL_DEFINITIONS = [
76
+ {
77
+ name: "setting.get",
78
+ description: [
79
+ "Fetch one or more Fluent Commerce settings by name (supports % wildcards).",
80
+ "",
81
+ "Returns metadata inline (name, context, contextId, valueType).",
82
+ "For small settings, the value is returned inline.",
83
+ "For large settings (manifests, JSON > 4KB), use outputFile to save content",
84
+ "to a local file and keep it out of the LLM context.",
85
+ "",
86
+ "When outputFile is set AND exactly one setting matches:",
87
+ " - Writes lobValue (or value) to the file",
88
+ " - Returns metadata + file path + byte size (NOT the content)",
89
+ "",
90
+ "When outputFile is NOT set:",
91
+ " - Returns full setting data inline (may be large for manifests)",
92
+ "",
93
+ "Examples:",
94
+ ' setting.get({ name: "fc.mystique.manifest.oms.fragment.ordermanagement" })',
95
+ ' setting.get({ name: "fc.mystique.manifest.%", context: "ACCOUNT" })',
96
+ ' setting.get({ name: "fc.mystique.manifest.oms", outputFile: "./manifest-backup.json" })',
97
+ ].join("\n"),
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ name: {
102
+ type: "string",
103
+ description: "Setting name or pattern with % wildcards",
104
+ },
105
+ context: {
106
+ type: "string",
107
+ description: "Filter by context: ACCOUNT, RETAILER, etc.",
108
+ },
109
+ contextId: {
110
+ type: "integer",
111
+ description: "Context ID. Defaults to FLUENT_RETAILER_ID or 0.",
112
+ },
113
+ outputFile: {
114
+ type: "string",
115
+ description: "File path to save setting content. Returns metadata only (not content) when set.",
116
+ },
117
+ first: {
118
+ type: "integer",
119
+ description: "Max results (default: 10, max: 100)",
120
+ default: 10,
121
+ },
122
+ },
123
+ required: ["name"],
124
+ additionalProperties: false,
125
+ },
126
+ },
41
127
  {
42
128
  name: "setting.upsert",
43
129
  description: [
@@ -50,6 +136,8 @@ export const SETTING_TOOL_DEFINITIONS = [
50
136
  '- context is a plain String ("RETAILER"), NOT an object',
51
137
  "- contextId is a separate Int field",
52
138
  "- For large JSON values (>4KB), use lobValue instead of value",
139
+ '- For Mystique manifest settings (fc.mystique.*), set valueType: "JSON"',
140
+ " — without it, defaults to LOB which breaks manifest parsing",
53
141
  "- Returns created: true/false for audit trail",
54
142
  "",
55
143
  "CONTEXTS: RETAILER, ACCOUNT, LOCATION, NETWORK, AGENT, CUSTOMER",
@@ -69,6 +157,12 @@ export const SETTING_TOOL_DEFINITIONS = [
69
157
  type: "string",
70
158
  description: "Large object value (for JSON payloads > 4KB)",
71
159
  },
160
+ valueType: {
161
+ type: "string",
162
+ enum: ["STRING", "LOB", "JSON"],
163
+ description: 'Explicit value type. Use "JSON" for Mystique manifest settings (fc.mystique.*). ' +
164
+ "Defaults to LOB when lobValue provided, STRING when value provided.",
165
+ },
72
166
  context: {
73
167
  type: "string",
74
168
  description: 'Setting scope: RETAILER, ACCOUNT, LOCATION, NETWORK, AGENT, CUSTOMER',
@@ -103,6 +197,11 @@ export const SETTING_TOOL_DEFINITIONS = [
103
197
  name: { type: "string" },
104
198
  value: { type: "string" },
105
199
  lobValue: { type: "string" },
200
+ valueType: {
201
+ type: "string",
202
+ enum: ["STRING", "LOB", "JSON"],
203
+ description: 'Explicit value type. Use "JSON" for manifest settings.',
204
+ },
106
205
  context: { type: "string" },
107
206
  contextId: { type: "integer" },
108
207
  },
@@ -179,10 +278,18 @@ async function createSetting(client, input) {
179
278
  };
180
279
  if (input.lobValue) {
181
280
  mutationInput.lobValue = input.lobValue;
182
- mutationInput.valueType = "LOB";
183
281
  }
184
282
  else {
185
283
  mutationInput.value = input.value;
284
+ }
285
+ // Explicit valueType wins; otherwise default to LOB/STRING based on field used
286
+ if (input.valueType) {
287
+ mutationInput.valueType = input.valueType;
288
+ }
289
+ else if (input.lobValue) {
290
+ mutationInput.valueType = "LOB";
291
+ }
292
+ else {
186
293
  mutationInput.valueType = "STRING";
187
294
  }
188
295
  const mutation = `mutation CreateSetting($input: CreateSettingInput!) {
@@ -220,10 +327,18 @@ async function updateSetting(client, input) {
220
327
  };
221
328
  if (input.lobValue) {
222
329
  mutationInput.lobValue = input.lobValue;
223
- mutationInput.valueType = "LOB";
224
330
  }
225
331
  else {
226
332
  mutationInput.value = input.value;
333
+ }
334
+ // Explicit valueType wins; otherwise default to LOB/STRING based on field used
335
+ if (input.valueType) {
336
+ mutationInput.valueType = input.valueType;
337
+ }
338
+ else if (input.lobValue) {
339
+ mutationInput.valueType = "LOB";
340
+ }
341
+ else {
227
342
  mutationInput.valueType = "STRING";
228
343
  }
229
344
  const mutation = `mutation UpdateSetting($input: UpdateSettingInput!) {
@@ -250,6 +365,151 @@ async function updateSetting(client, input) {
250
365
  }
251
366
  return setting;
252
367
  }
368
+ /**
369
+ * Handle setting.get tool call.
370
+ * Fetches settings by name (with wildcard support) and optionally writes to file.
371
+ */
372
+ export async function handleSettingGet(args, ctx) {
373
+ const parsed = SettingGetInputSchema.parse(args);
374
+ const client = requireSettingClient(ctx);
375
+ // Build variables — only include context/contextId if provided
376
+ const variables = {
377
+ name: [parsed.name],
378
+ first: parsed.first,
379
+ };
380
+ if (parsed.context) {
381
+ variables.context = [parsed.context];
382
+ }
383
+ if (parsed.contextId !== undefined) {
384
+ variables.contextId = [parsed.contextId];
385
+ }
386
+ else if (parsed.context === "RETAILER" && ctx.config.retailerId) {
387
+ variables.contextId = [Number(ctx.config.retailerId)];
388
+ }
389
+ else if (parsed.context === "ACCOUNT") {
390
+ variables.contextId = [0];
391
+ }
392
+ // Build query — include lobValue for full content
393
+ const query = `query GetSettings($name: [String!], $context: [String!], $contextId: [Int!], $first: Int) {
394
+ settings(name: $name, context: $context, contextId: $contextId, first: $first) {
395
+ edges {
396
+ node {
397
+ id
398
+ name
399
+ value
400
+ lobValue
401
+ context
402
+ contextId
403
+ valueType
404
+ }
405
+ }
406
+ }
407
+ }`;
408
+ const response = await client.graphql({
409
+ query,
410
+ variables: variables,
411
+ });
412
+ const data = response?.data;
413
+ const connection = data?.settings;
414
+ const edges = (connection?.edges ?? []);
415
+ const settings = edges.map((e) => e.node);
416
+ if (settings.length === 0) {
417
+ return {
418
+ ok: true,
419
+ count: 0,
420
+ settings: [],
421
+ message: `No settings found matching "${parsed.name}"${parsed.context ? ` with context ${parsed.context}` : ""}.`,
422
+ };
423
+ }
424
+ // If outputFile is provided and exactly one setting matches, write to file
425
+ if (parsed.outputFile && settings.length === 1) {
426
+ const setting = settings[0];
427
+ const content = setting.lobValue ?? setting.value ?? "";
428
+ // Try to pretty-print JSON content
429
+ let fileContent = content;
430
+ try {
431
+ const parsed = JSON.parse(content);
432
+ fileContent = JSON.stringify(parsed, null, 2);
433
+ }
434
+ catch {
435
+ // Not JSON — write as-is
436
+ }
437
+ // Ensure parent directory exists
438
+ const dir = path.dirname(parsed.outputFile);
439
+ fs.mkdirSync(dir, { recursive: true });
440
+ fs.writeFileSync(parsed.outputFile, fileContent, "utf-8");
441
+ return {
442
+ ok: true,
443
+ count: 1,
444
+ savedTo: parsed.outputFile,
445
+ sizeBytes: Buffer.byteLength(fileContent, "utf-8"),
446
+ setting: {
447
+ id: setting.id,
448
+ name: setting.name,
449
+ context: setting.context,
450
+ contextId: setting.contextId,
451
+ valueType: setting.valueType,
452
+ },
453
+ message: `Setting "${setting.name}" saved to ${parsed.outputFile} (${Buffer.byteLength(fileContent, "utf-8")} bytes). Content NOT included in response.`,
454
+ };
455
+ }
456
+ // If outputFile provided but multiple settings match, warn
457
+ if (parsed.outputFile && settings.length > 1) {
458
+ // Write each setting to a separate file in the outputFile directory
459
+ const outputDir = parsed.outputFile.endsWith(".json")
460
+ ? path.dirname(parsed.outputFile)
461
+ : parsed.outputFile;
462
+ fs.mkdirSync(outputDir, { recursive: true });
463
+ const savedFiles = [];
464
+ for (const setting of settings) {
465
+ const content = setting.lobValue ?? setting.value ?? "";
466
+ let fileContent = content;
467
+ try {
468
+ const p = JSON.parse(content);
469
+ fileContent = JSON.stringify(p, null, 2);
470
+ }
471
+ catch {
472
+ // Not JSON — write as-is
473
+ }
474
+ const safeName = setting.name.replace(/[^a-zA-Z0-9._-]/g, "_");
475
+ const filePath = path.join(outputDir, `${safeName}.json`);
476
+ fs.writeFileSync(filePath, fileContent, "utf-8");
477
+ savedFiles.push({
478
+ name: setting.name,
479
+ file: filePath,
480
+ sizeBytes: Buffer.byteLength(fileContent, "utf-8"),
481
+ });
482
+ }
483
+ return {
484
+ ok: true,
485
+ count: settings.length,
486
+ savedTo: outputDir,
487
+ files: savedFiles,
488
+ settings: settings.map((s) => ({
489
+ id: s.id,
490
+ name: s.name,
491
+ context: s.context,
492
+ contextId: s.contextId,
493
+ valueType: s.valueType,
494
+ })),
495
+ message: `${settings.length} settings saved to ${outputDir}/. Content NOT included in response.`,
496
+ };
497
+ }
498
+ // No outputFile — return everything inline
499
+ return {
500
+ ok: true,
501
+ count: settings.length,
502
+ settings: settings.map((s) => ({
503
+ id: s.id,
504
+ name: s.name,
505
+ value: s.value,
506
+ lobValue: s.lobValue,
507
+ context: s.context,
508
+ contextId: s.contextId,
509
+ valueType: s.valueType,
510
+ })),
511
+ };
512
+ }
253
513
  /**
254
514
  * Handle setting.upsert tool call.
255
515
  */
@@ -259,37 +519,52 @@ export async function handleSettingUpsert(args, ctx) {
259
519
  const contextId = resolveContextId(parsed.context, parsed.contextId, ctx.config);
260
520
  // Check if setting already exists
261
521
  const existing = await findExistingSetting(client, parsed.name, parsed.context, contextId);
522
+ // Warn when manifest setting is used without explicit valueType: "JSON"
523
+ const isManifestSetting = parsed.name.startsWith("fc.mystique.manifest.");
524
+ const manifestWarning = isManifestSetting && parsed.valueType !== "JSON"
525
+ ? 'Manifest setting detected without valueType:"JSON". ' +
526
+ "Default LOB type will break Mystique manifest parsing. " +
527
+ 'Set valueType:"JSON" for fc.mystique.* settings.'
528
+ : undefined;
262
529
  if (existing) {
263
530
  // Update
264
531
  const setting = await updateSetting(client, {
265
532
  id: existing.id,
266
533
  value: parsed.value,
267
534
  lobValue: parsed.lobValue,
535
+ valueType: parsed.valueType,
268
536
  context: parsed.context,
269
537
  contextId,
270
538
  });
271
- return {
539
+ const result = {
272
540
  ok: true,
273
541
  created: false,
274
542
  updated: true,
275
543
  setting,
276
544
  previousValue: existing.value ?? existing.lobValue,
277
545
  };
546
+ if (manifestWarning)
547
+ result.warning = manifestWarning;
548
+ return result;
278
549
  }
279
550
  // Create
280
551
  const setting = await createSetting(client, {
281
552
  name: parsed.name,
282
553
  value: parsed.value,
283
554
  lobValue: parsed.lobValue,
555
+ valueType: parsed.valueType,
284
556
  context: parsed.context,
285
557
  contextId,
286
558
  });
287
- return {
559
+ const result = {
288
560
  ok: true,
289
561
  created: true,
290
562
  updated: false,
291
563
  setting,
292
564
  };
565
+ if (manifestWarning)
566
+ result.warning = manifestWarning;
567
+ return result;
293
568
  }
294
569
  /**
295
570
  * Handle setting.bulkUpsert tool call.
@@ -310,6 +585,7 @@ export async function handleSettingBulkUpsert(args, ctx) {
310
585
  id: existing.id,
311
586
  value: setting.value,
312
587
  lobValue: setting.lobValue,
588
+ valueType: setting.valueType,
313
589
  context: setting.context,
314
590
  contextId,
315
591
  });
@@ -321,6 +597,7 @@ export async function handleSettingBulkUpsert(args, ctx) {
321
597
  name: setting.name,
322
598
  value: setting.value,
323
599
  lobValue: setting.lobValue,
600
+ valueType: setting.valueType,
324
601
  context: setting.context,
325
602
  contextId,
326
603
  });
package/dist/tools.js CHANGED
@@ -6,8 +6,8 @@ import { buildEventPayload } from "./event-payload.js";
6
6
  import { ToolError, toToolFailure } from "./errors.js";
7
7
  // New tool modules
8
8
  import { ENTITY_TOOL_DEFINITIONS, handleEntityCreate, handleEntityUpdate, handleEntityGet, } from "./entity-tools.js";
9
- import { WORKFLOW_TOOL_DEFINITIONS, handleWorkflowUpload, handleWorkflowDiff, handleWorkflowSimulate, } from "./workflow-tools.js";
10
- import { SETTING_TOOL_DEFINITIONS, handleSettingUpsert, handleSettingBulkUpsert, } from "./settings-tools.js";
9
+ import { WORKFLOW_TOOL_DEFINITIONS, handleWorkflowGet, handleWorkflowList, handleWorkflowUpload, handleWorkflowDiff, handleWorkflowSimulate, } from "./workflow-tools.js";
10
+ import { SETTING_TOOL_DEFINITIONS, handleSettingGet, handleSettingUpsert, handleSettingBulkUpsert, } from "./settings-tools.js";
11
11
  import { ENVIRONMENT_TOOL_DEFINITIONS, handleEnvironmentDiscover, handleEnvironmentValidate, } from "./environment-tools.js";
12
12
  import { TEST_TOOL_DEFINITIONS, handleTestAssert, } from "./test-tools.js";
13
13
  import { shapeResponse, summarizeConnection, analyzeEvents, } from "./response-shaper.js";
@@ -128,6 +128,8 @@ const TransitionTriggerInputSchema = z.object({
128
128
  module: z.string().min(1).optional(),
129
129
  flexType: z.string().min(1).optional(),
130
130
  flexVersion: z.union([z.string().min(1), z.number().int()]).optional(),
131
+ entityId: z.string().min(1).optional(),
132
+ entityRef: z.string().min(1).optional(),
131
133
  });
132
134
  const TransitionActionsInputSchema = z.object({
133
135
  triggers: z.array(TransitionTriggerInputSchema).min(1),
@@ -193,6 +195,7 @@ const GraphQLBatchMutateInputSchema = z.object({
193
195
  inputs: z.array(z.record(z.string(), z.unknown())).min(1).max(50),
194
196
  returnFields: z.array(z.string()).optional(),
195
197
  operationName: z.string().optional(),
198
+ dryRun: z.boolean().default(false),
196
199
  });
197
200
  const GraphQLIntrospectInputSchema = z.object({
198
201
  type: z.string().optional(),
@@ -598,8 +601,16 @@ const TOOL_DEFINITIONS = [
598
601
  "INTEGRATION: The eventName from each userAction maps directly to event.send's \"name\" parameter.",
599
602
  "The attributes[] tell you what to include in event.send's \"attributes\" parameter.",
600
603
  "",
604
+ "CRITICAL: The Transition API requires `flexType` (e.g. ORDER::HD) to return results.",
605
+ "Without it the API silently returns empty `{response:[]}`. For some entity types",
606
+ "(e.g. CREDIT_MEMO) `flexVersion` is also required. When `type` and `subtype` are",
607
+ "provided but `flexType` is not, the tool auto-derives it as `TYPE::SUBTYPE`.",
608
+ "",
609
+ "BEST PRACTICE: Always provide `flexType` and `flexVersion` for reliable results.",
610
+ "Get these from the entity's `workflowRef` and `workflowVersion` fields via GraphQL.",
611
+ "",
601
612
  "EXAMPLE: Find available actions for an ORDER in CREATED status:",
602
- "{ triggers: [{ type: \"ORDER\", subtype: \"HD\", status: \"CREATED\", retailerId: \"5\", flexType: \"ORDER::HD\" }] }",
613
+ "{ triggers: [{ type: \"ORDER\", subtype: \"HD\", status: \"CREATED\", retailerId: \"5\", flexType: \"ORDER::HD\", flexVersion: 4 }] }",
603
614
  ].join("\n"),
604
615
  inputSchema: {
605
616
  type: "object",
@@ -622,17 +633,19 @@ const TOOL_DEFINITIONS = [
622
633
  },
623
634
  module: {
624
635
  type: "string",
625
- description: "Filter by module (e.g. servicepoint, adminconsole). Case-sensitive.",
636
+ description: "REQUIRED for results. App module name (e.g. adminconsole, oms, store, servicepoint). Without this, API silently returns empty userActions. Case-sensitive.",
626
637
  },
627
638
  flexType: {
628
639
  type: "string",
629
- description: "Workflow type (e.g. ORDER::HD, FULFILMENT::HD_WH).",
640
+ description: "Workflow type (e.g. ORDER::HD, FULFILMENT::HD_WH). REQUIRED for API to return results. Auto-derived from type::subtype when omitted.",
630
641
  },
631
642
  flexVersion: {
632
643
  oneOf: [{ type: "integer" }, { type: "string" }],
633
- description: "Workflow version. Omit for latest.",
644
+ description: "Workflow version. Strongly recommended — required for some entity types (e.g. CREDIT_MEMO). Get from entity's workflowVersion field.",
634
645
  },
635
646
  name: { type: "string", description: "Event name filter" },
647
+ entityId: { type: "string", description: "Entity ID (strongly recommended — API may return empty without it)" },
648
+ entityRef: { type: "string", description: "Entity ref (strongly recommended — API may return empty without it)" },
636
649
  },
637
650
  required: ["retailerId"],
638
651
  additionalProperties: false,
@@ -888,6 +901,10 @@ const TOOL_DEFINITIONS = [
888
901
  type: "string",
889
902
  description: "Custom GraphQL operation name (default: Batch<MutationName>s).",
890
903
  },
904
+ dryRun: {
905
+ type: "boolean",
906
+ description: "If true, builds and validates the mutation query without executing it. Returns the query, variables, and input count for review. Default: false.",
907
+ },
891
908
  },
892
909
  required: ["mutation", "inputs"],
893
910
  additionalProperties: false,
@@ -1253,7 +1270,14 @@ export function toEventQueryParams(input) {
1253
1270
  return params;
1254
1271
  }
1255
1272
  /**
1256
- * Fill missing trigger.retailerId from runtime config and validate readiness.
1273
+ * Fill missing trigger fields from runtime config and validate readiness.
1274
+ *
1275
+ * The Transition API requires `flexType` (e.g. "ORDER::HD") to return results.
1276
+ * Without it the API silently returns `{"response":[]}`. For some entity
1277
+ * types (e.g. CREDIT_MEMO) `flexVersion` is also required.
1278
+ *
1279
+ * This function auto-derives `flexType` from `type`+`subtype` when both are
1280
+ * present and `flexType` is not explicitly set.
1257
1281
  */
1258
1282
  export function resolveTransitionRequest(input, fallbackRetailerId) {
1259
1283
  const triggers = input.triggers.map((trigger, index) => {
@@ -1261,9 +1285,16 @@ export function resolveTransitionRequest(input, fallbackRetailerId) {
1261
1285
  if (!retailerId) {
1262
1286
  throw new ToolError("VALIDATION_ERROR", `triggers[${index}].retailerId is required. Set FLUENT_RETAILER_ID or pass retailerId per trigger.`);
1263
1287
  }
1288
+ // Auto-derive flexType from type+subtype when not explicitly provided.
1289
+ // The Transition API requires flexType to return user actions.
1290
+ let flexType = trigger.flexType;
1291
+ if (!flexType && trigger.type && trigger.subtype) {
1292
+ flexType = `${trigger.type}::${trigger.subtype}`;
1293
+ }
1264
1294
  return {
1265
1295
  ...trigger,
1266
1296
  retailerId,
1297
+ ...(flexType ? { flexType } : {}),
1267
1298
  };
1268
1299
  });
1269
1300
  return { triggers };
@@ -2540,6 +2571,18 @@ export function registerToolHandlers(server, ctx) {
2540
2571
  for (let i = 0; i < batchSize; i++) {
2541
2572
  variables[`input${i + 1}`] = parsed.inputs[i];
2542
2573
  }
2574
+ // Dry-run: return query + variables without executing
2575
+ if (parsed.dryRun) {
2576
+ return json({
2577
+ ok: true,
2578
+ dryRun: true,
2579
+ mutation: parsed.mutation,
2580
+ inputCount: batchSize,
2581
+ query,
2582
+ variables,
2583
+ note: "No API call made. Set dryRun=false to execute.",
2584
+ });
2585
+ }
2543
2586
  const gqlPayload = {
2544
2587
  query,
2545
2588
  variables: toSdkVariables(variables),
@@ -3195,6 +3238,16 @@ export function registerToolHandlers(server, ctx) {
3195
3238
  const result = await handleEntityGet(args, ctx);
3196
3239
  return json(result, false, ctx.responseBudget);
3197
3240
  }
3241
+ // ------- workflow.get ---------------------------------------------------
3242
+ if (toolName === "workflow.get") {
3243
+ const result = await handleWorkflowGet(args, ctx);
3244
+ return json(result, false, ctx.responseBudget);
3245
+ }
3246
+ // ------- workflow.list --------------------------------------------------
3247
+ if (toolName === "workflow.list") {
3248
+ const result = await handleWorkflowList(args, ctx);
3249
+ return json(result, false, ctx.responseBudget);
3250
+ }
3198
3251
  // ------- workflow.upload -----------------------------------------------
3199
3252
  if (toolName === "workflow.upload") {
3200
3253
  const result = await handleWorkflowUpload(args, ctx);
@@ -3210,6 +3263,11 @@ export function registerToolHandlers(server, ctx) {
3210
3263
  const result = await handleWorkflowSimulate(args, ctx);
3211
3264
  return json(result, false, ctx.responseBudget);
3212
3265
  }
3266
+ // ------- setting.get ---------------------------------------------------
3267
+ if (toolName === "setting.get") {
3268
+ const result = await handleSettingGet(args, ctx);
3269
+ return json(result, false, ctx.responseBudget);
3270
+ }
3213
3271
  // ------- setting.upsert ------------------------------------------------
3214
3272
  if (toolName === "setting.upsert") {
3215
3273
  const result = await handleSettingUpsert(args, ctx);