@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.
- package/README.md +924 -804
- package/dist/entity-registry.js +1 -0
- package/dist/fluent-client.js +42 -12
- package/dist/settings-tools.js +281 -4
- package/dist/tools.js +65 -7
- package/dist/workflow-tools.js +286 -0
- package/docs/CONTRIBUTING.md +100 -100
- package/docs/E2E_TESTING.md +739 -429
- package/docs/HANDOVER_ENV.example +29 -29
- package/docs/HANDOVER_GITHUB_COPILOT.md +165 -165
- package/docs/RUNBOOK.md +315 -233
- package/docs/TOOL_REFERENCE.md +1923 -1156
- package/package.json +68 -64
package/dist/entity-registry.js
CHANGED
|
@@ -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,
|
package/dist/fluent-client.js
CHANGED
|
@@ -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) {
|
package/dist/settings-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
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
|
|
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);
|