@fluentcommerce/fluent-mcp-extn 0.2.1 → 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 +112 -11
- package/dist/entity-registry.js +1 -0
- package/dist/fluent-client.js +30 -0
- package/dist/settings-tools.js +281 -4
- package/dist/tools.js +48 -7
- package/dist/workflow-tools.js +286 -0
- package/docs/RUNBOOK.md +5 -2
- package/docs/TOOL_REFERENCE.md +116 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Exposes event dispatch, transition actions, GraphQL operations, Prometheus metri
|
|
|
19
19
|
- [Requirements](#requirements)
|
|
20
20
|
- [Install](#install)
|
|
21
21
|
- [Configuration](#configuration)
|
|
22
|
-
- [Tools (
|
|
22
|
+
- [Tools (39)](#tools-39)
|
|
23
23
|
- [Error Handling](#error-handling)
|
|
24
24
|
- [Support Scripts (Debugging and Validation)](#support-scripts-debugging-and-validation)
|
|
25
25
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -81,8 +81,8 @@ The official `fluent mcp server` (bundled with Fluent CLI) covers core GraphQL a
|
|
|
81
81
|
| Schema introspection (`graphql.introspect`) | | Yes |
|
|
82
82
|
| Multi-strategy auth (profile, OAuth, token command, static token) | | Yes |
|
|
83
83
|
| Entity CRUD (`entity.create` / `entity.update` / `entity.get`) | | Yes |
|
|
84
|
-
| Workflow management (`workflow.upload` / `workflow.diff` / `workflow.simulate`) | | Yes |
|
|
85
|
-
| Settings management (`setting.upsert` / `setting.bulkUpsert`) | | Yes |
|
|
84
|
+
| Workflow management (`workflow.get` / `workflow.list` / `workflow.upload` / `workflow.diff` / `workflow.simulate`) | | Yes |
|
|
85
|
+
| Settings management (`setting.get` / `setting.upsert` / `setting.bulkUpsert`) | | Yes |
|
|
86
86
|
| Environment snapshot (`environment.discover` / `environment.validate`) | | Yes |
|
|
87
87
|
| Test assertions with polling (`test.assert`) | | Yes |
|
|
88
88
|
|
|
@@ -120,7 +120,16 @@ Reads base URL, client ID/secret, username/password, and retailer ID from `~/.fl
|
|
|
120
120
|
|
|
121
121
|
#### Option B: Explicit OAuth credentials
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Set credentials as **shell environment variables** (never in `.mcp.json`):
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
export FLUENT_CLIENT_ID=your-client-id
|
|
127
|
+
export FLUENT_CLIENT_SECRET=your-client-secret
|
|
128
|
+
export FLUENT_USERNAME=your-username
|
|
129
|
+
export FLUENT_PASSWORD=your-password
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Then in `.mcp.json`, only the non-sensitive connection details:
|
|
124
133
|
|
|
125
134
|
```json
|
|
126
135
|
{
|
|
@@ -131,17 +140,15 @@ Pass all credentials directly in env vars:
|
|
|
131
140
|
"args": ["@fluentcommerce/fluent-mcp-extn"],
|
|
132
141
|
"env": {
|
|
133
142
|
"FLUENT_BASE_URL": "https://YOUR_ACCOUNT.sandbox.api.fluentretail.com",
|
|
134
|
-
"FLUENT_RETAILER_ID": "YOUR_RETAILER_ID"
|
|
135
|
-
"FLUENT_CLIENT_ID": "YOUR_CLIENT_ID",
|
|
136
|
-
"FLUENT_CLIENT_SECRET": "YOUR_CLIENT_SECRET",
|
|
137
|
-
"FLUENT_USERNAME": "YOUR_USERNAME",
|
|
138
|
-
"FLUENT_PASSWORD": "YOUR_PASSWORD"
|
|
143
|
+
"FLUENT_RETAILER_ID": "YOUR_RETAILER_ID"
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
```
|
|
144
149
|
|
|
150
|
+
The MCP server inherits all parent shell environment variables automatically.
|
|
151
|
+
|
|
145
152
|
#### Option C: Profile + overrides (hybrid)
|
|
146
153
|
|
|
147
154
|
Use a profile as the base, override specific values via env vars:
|
|
@@ -383,6 +390,8 @@ Plus at least one auth method below.
|
|
|
383
390
|
| `FLUENT_USERNAME` | No | Username (for password grant) |
|
|
384
391
|
| `FLUENT_PASSWORD` | No | Password (for password grant) |
|
|
385
392
|
|
|
393
|
+
> **Security:** These credentials must be set as shell environment variables, never in `.mcp.json`. MCP server processes inherit all parent shell env vars automatically. The `mcp-setup` command strips any secrets found in `.mcp.json` during re-runs.
|
|
394
|
+
|
|
386
395
|
#### 3. Token command
|
|
387
396
|
|
|
388
397
|
| Variable | Default | Description |
|
|
@@ -423,7 +432,7 @@ Controls how large responses are handled before returning to the AI. When a resp
|
|
|
423
432
|
| `FLUENT_RESPONSE_MAX_ARRAY` | `50` | Arrays exceeding this size are auto-summarized. Only active when budget > 0. |
|
|
424
433
|
| `FLUENT_RESPONSE_SAMPLE_SIZE` | `3` | Number of sample records included in array summaries. |
|
|
425
434
|
|
|
426
|
-
## Tools (
|
|
435
|
+
## Tools (39)
|
|
427
436
|
|
|
428
437
|
### Diagnostics
|
|
429
438
|
|
|
@@ -534,6 +543,8 @@ Useful flags:
|
|
|
534
543
|
| `workflow.transitions` | Query available user actions/transitions for provided triggers (`POST /api/v4.1/transition`) |
|
|
535
544
|
| `plugin.list` | List all registered rules with metadata (`GET /orchestration/rest/v1/plugin`). Supports optional name filter. |
|
|
536
545
|
|
|
546
|
+
**`flexType` auto-derive:** When `type` and `subtype` are provided but `flexType` is omitted, it is auto-derived as `TYPE::SUBTYPE`. All three params (`module`, `flexType`, `flexVersion`) are required by the Transition API — omitting any one silently returns empty results.
|
|
547
|
+
|
|
537
548
|
**Example** — get actions for a ServicePoint manifest trigger:
|
|
538
549
|
|
|
539
550
|
```json
|
|
@@ -551,6 +562,23 @@ Useful flags:
|
|
|
551
562
|
}
|
|
552
563
|
```
|
|
553
564
|
|
|
565
|
+
**Example** — auto-derive flexType from type + subtype:
|
|
566
|
+
|
|
567
|
+
```json
|
|
568
|
+
{
|
|
569
|
+
"triggers": [
|
|
570
|
+
{
|
|
571
|
+
"type": "CREDIT_MEMO",
|
|
572
|
+
"subtype": "APPEASEMENT",
|
|
573
|
+
"status": "CREATED",
|
|
574
|
+
"module": "adminconsole",
|
|
575
|
+
"flexVersion": "1.0",
|
|
576
|
+
"retailerId": "2"
|
|
577
|
+
}
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
554
582
|
**Example** — list all rules matching "SendEvent":
|
|
555
583
|
|
|
556
584
|
```json
|
|
@@ -600,6 +628,19 @@ Useful flags:
|
|
|
600
628
|
}
|
|
601
629
|
```
|
|
602
630
|
|
|
631
|
+
**Example** — dry-run batch mutation (validates query without executing):
|
|
632
|
+
|
|
633
|
+
```json
|
|
634
|
+
{
|
|
635
|
+
"mutation": "updateOrder",
|
|
636
|
+
"inputs": [{ "id": "1", "status": "SHIPPED" }],
|
|
637
|
+
"returnFields": ["id", "ref", "status"],
|
|
638
|
+
"dryRun": true
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
With `dryRun: true`, returns the generated aliased mutation query and variables for review without making any API call. Use this to preview what will be sent before executing bulk operations.
|
|
643
|
+
|
|
603
644
|
### Batch Ingestion
|
|
604
645
|
|
|
605
646
|
| Tool | Description |
|
|
@@ -647,19 +688,73 @@ Typical flow: `batch.create` → `batch.send` → `batch.status` (poll) → `bat
|
|
|
647
688
|
|
|
648
689
|
| Tool | Description |
|
|
649
690
|
|---|---|
|
|
691
|
+
| `workflow.get` | Fetch a specific workflow by entity type and subtype via REST API. Works even when the list endpoint returns 401. |
|
|
692
|
+
| `workflow.list` | List all workflows for a retailer. Deduplicates to latest version per workflow name. |
|
|
650
693
|
| `workflow.upload` | Deploy workflow JSON via REST API with structure validation. For production, prefer `fluent module install` via CLI. |
|
|
651
694
|
| `workflow.diff` | Compare two workflow definitions — returns added/removed/modified rulesets with risk assessment. Supports `summary`, `detailed`, and `mermaid` formats. |
|
|
652
695
|
| `workflow.simulate` | Static analysis prediction of which rulesets would fire for a given status + event name. Does NOT execute Java rules or check runtime state — use `workflow.transitions` for authoritative live validation. |
|
|
653
696
|
|
|
697
|
+
**Example** — fetch a workflow and save to file:
|
|
698
|
+
|
|
699
|
+
```json
|
|
700
|
+
{
|
|
701
|
+
"entityType": "ORDER",
|
|
702
|
+
"entitySubtype": "HD",
|
|
703
|
+
"retailerId": "2",
|
|
704
|
+
"outputFile": "accounts/MYPROFILE/workflows/MyRetailer/ORDER-HD.json"
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
When `outputFile` is set, the full workflow JSON is saved to disk and only a summary is returned (status count, ruleset count, total rules). This reads from the **live server** via REST API — not the CLI workflowlog cache.
|
|
709
|
+
|
|
710
|
+
**Example** — list all workflows and download to directory:
|
|
711
|
+
|
|
712
|
+
```json
|
|
713
|
+
{
|
|
714
|
+
"retailerId": "2",
|
|
715
|
+
"outputDir": "accounts/MYPROFILE/workflows/MyRetailer/"
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
With `outputDir`, each workflow is saved as `{TYPE}-{SUBTYPE}.json` (e.g., `ORDER-HD.json`). The response lists saved files with paths and sizes. Without `outputDir`, returns metadata only (name, version, status count per workflow).
|
|
720
|
+
|
|
654
721
|
### Settings Management
|
|
655
722
|
|
|
656
723
|
| Tool | Description |
|
|
657
724
|
|---|---|
|
|
725
|
+
| `setting.get` | Fetch settings by name (`%` wildcards supported), optionally save to local file to keep large JSON out of LLM context |
|
|
658
726
|
| `setting.upsert` | Create or update a setting with upsert semantics — queries existing by name + context + contextId first |
|
|
659
727
|
| `setting.bulkUpsert` | Batch create/update up to 50 settings with per-setting error handling |
|
|
660
728
|
|
|
661
729
|
**Contexts:** RETAILER, ACCOUNT, LOCATION, NETWORK, AGENT, CUSTOMER
|
|
662
730
|
|
|
731
|
+
**Example** — fetch a manifest setting and save to file (keeps large JSON out of LLM context):
|
|
732
|
+
|
|
733
|
+
```json
|
|
734
|
+
{
|
|
735
|
+
"name": "fc.mystique.manifest.oms",
|
|
736
|
+
"context": "ACCOUNT",
|
|
737
|
+
"contextId": 0,
|
|
738
|
+
"outputFile": "accounts/MYPROFILE/manifests/backups/fc.mystique.manifest.oms.json"
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
When `outputFile` is set, the response returns metadata only (name, context, valueType, sizeBytes, savedTo path) — the full JSON goes to disk. For multiple matches (e.g., `name: "fc.mystique.manifest.%"`), each setting is saved as a separate file in the outputFile directory.
|
|
743
|
+
|
|
744
|
+
**Example** — create/update a manifest setting with explicit `valueType`:
|
|
745
|
+
|
|
746
|
+
```json
|
|
747
|
+
{
|
|
748
|
+
"name": "fc.mystique.manifest.oms.fragment.custom",
|
|
749
|
+
"context": "ACCOUNT",
|
|
750
|
+
"contextId": 0,
|
|
751
|
+
"valueType": "JSON",
|
|
752
|
+
"lobValue": { "manifestVersion": "2.0", "routes": [] }
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
The `valueType` parameter accepts `"STRING"`, `"LOB"`, or `"JSON"`. For Mystique manifest settings, always use `valueType: "JSON"` — the default `LOB` breaks manifest parsing. A warning is emitted if a `fc.mystique.manifest.*` setting is created without `valueType: "JSON"`.
|
|
757
|
+
|
|
663
758
|
### Environment
|
|
664
759
|
|
|
665
760
|
| Tool | Description |
|
|
@@ -727,6 +822,10 @@ All tools return a consistent envelope:
|
|
|
727
822
|
| `SDK_ERROR` | Unexpected SDK error | Varies |
|
|
728
823
|
| `UNKNOWN_ERROR` | Unclassified error | No |
|
|
729
824
|
|
|
825
|
+
### Retry Behavior
|
|
826
|
+
|
|
827
|
+
Read operations (queries, list, get) use `execute()` which retries on transient failures (timeout, rate limit, network errors) with exponential backoff up to `FLUENT_RETRY_ATTEMPTS` times. Write operations (create, update, send, upload, upsert) use `executeOnce()` with **no automatic retry** to prevent duplicate side effects. Tune retry parameters via the [Resilience Tuning](#resilience-tuning) env vars above.
|
|
828
|
+
|
|
730
829
|
## Support Scripts (Debugging and Validation)
|
|
731
830
|
|
|
732
831
|
These scripts are useful during tenant validation, support, and failure triage:
|
|
@@ -775,7 +874,9 @@ When used alongside `@fluentcommerce/ai-skills`, this extension server integrate
|
|
|
775
874
|
```
|
|
776
875
|
accounts/
|
|
777
876
|
<PROFILE>/
|
|
778
|
-
SOURCE/ # custom
|
|
877
|
+
SOURCE/ # custom source code (account-level, shared across retailers)
|
|
878
|
+
backend/ # Java Maven plugin repos and JAR files
|
|
879
|
+
frontend/ # Mystique SDK component projects
|
|
779
880
|
workflows/ # downloaded workflow JSONs (scoped by retailer)
|
|
780
881
|
analysis/ # generated analysis artifacts
|
|
781
882
|
```
|
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
|
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),
|
|
@@ -599,8 +601,16 @@ const TOOL_DEFINITIONS = [
|
|
|
599
601
|
"INTEGRATION: The eventName from each userAction maps directly to event.send's \"name\" parameter.",
|
|
600
602
|
"The attributes[] tell you what to include in event.send's \"attributes\" parameter.",
|
|
601
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
|
+
"",
|
|
602
612
|
"EXAMPLE: Find available actions for an ORDER in CREATED status:",
|
|
603
|
-
"{ 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 }] }",
|
|
604
614
|
].join("\n"),
|
|
605
615
|
inputSchema: {
|
|
606
616
|
type: "object",
|
|
@@ -623,17 +633,19 @@ const TOOL_DEFINITIONS = [
|
|
|
623
633
|
},
|
|
624
634
|
module: {
|
|
625
635
|
type: "string",
|
|
626
|
-
description: "
|
|
636
|
+
description: "REQUIRED for results. App module name (e.g. adminconsole, oms, store, servicepoint). Without this, API silently returns empty userActions. Case-sensitive.",
|
|
627
637
|
},
|
|
628
638
|
flexType: {
|
|
629
639
|
type: "string",
|
|
630
|
-
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.",
|
|
631
641
|
},
|
|
632
642
|
flexVersion: {
|
|
633
643
|
oneOf: [{ type: "integer" }, { type: "string" }],
|
|
634
|
-
description: "Workflow version.
|
|
644
|
+
description: "Workflow version. Strongly recommended — required for some entity types (e.g. CREDIT_MEMO). Get from entity's workflowVersion field.",
|
|
635
645
|
},
|
|
636
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)" },
|
|
637
649
|
},
|
|
638
650
|
required: ["retailerId"],
|
|
639
651
|
additionalProperties: false,
|
|
@@ -1258,7 +1270,14 @@ export function toEventQueryParams(input) {
|
|
|
1258
1270
|
return params;
|
|
1259
1271
|
}
|
|
1260
1272
|
/**
|
|
1261
|
-
* 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.
|
|
1262
1281
|
*/
|
|
1263
1282
|
export function resolveTransitionRequest(input, fallbackRetailerId) {
|
|
1264
1283
|
const triggers = input.triggers.map((trigger, index) => {
|
|
@@ -1266,9 +1285,16 @@ export function resolveTransitionRequest(input, fallbackRetailerId) {
|
|
|
1266
1285
|
if (!retailerId) {
|
|
1267
1286
|
throw new ToolError("VALIDATION_ERROR", `triggers[${index}].retailerId is required. Set FLUENT_RETAILER_ID or pass retailerId per trigger.`);
|
|
1268
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
|
+
}
|
|
1269
1294
|
return {
|
|
1270
1295
|
...trigger,
|
|
1271
1296
|
retailerId,
|
|
1297
|
+
...(flexType ? { flexType } : {}),
|
|
1272
1298
|
};
|
|
1273
1299
|
});
|
|
1274
1300
|
return { triggers };
|
|
@@ -3212,6 +3238,16 @@ export function registerToolHandlers(server, ctx) {
|
|
|
3212
3238
|
const result = await handleEntityGet(args, ctx);
|
|
3213
3239
|
return json(result, false, ctx.responseBudget);
|
|
3214
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
|
+
}
|
|
3215
3251
|
// ------- workflow.upload -----------------------------------------------
|
|
3216
3252
|
if (toolName === "workflow.upload") {
|
|
3217
3253
|
const result = await handleWorkflowUpload(args, ctx);
|
|
@@ -3227,6 +3263,11 @@ export function registerToolHandlers(server, ctx) {
|
|
|
3227
3263
|
const result = await handleWorkflowSimulate(args, ctx);
|
|
3228
3264
|
return json(result, false, ctx.responseBudget);
|
|
3229
3265
|
}
|
|
3266
|
+
// ------- setting.get ---------------------------------------------------
|
|
3267
|
+
if (toolName === "setting.get") {
|
|
3268
|
+
const result = await handleSettingGet(args, ctx);
|
|
3269
|
+
return json(result, false, ctx.responseBudget);
|
|
3270
|
+
}
|
|
3230
3271
|
// ------- setting.upsert ------------------------------------------------
|
|
3231
3272
|
if (toolName === "setting.upsert") {
|
|
3232
3273
|
const result = await handleSettingUpsert(args, ctx);
|
package/dist/workflow-tools.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* event outcomes without live execution.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
10
12
|
import { ToolError } from "./errors.js";
|
|
11
13
|
// ---------------------------------------------------------------------------
|
|
12
14
|
// Input schemas
|
|
@@ -40,6 +42,42 @@ export const WorkflowDiffInputSchema = z.object({
|
|
|
40
42
|
.default("summary")
|
|
41
43
|
.describe("Output format: summary (default), detailed, or mermaid"),
|
|
42
44
|
});
|
|
45
|
+
export const WorkflowGetInputSchema = z.object({
|
|
46
|
+
entityType: z
|
|
47
|
+
.string()
|
|
48
|
+
.describe("Workflow entity type (e.g., ORDER, FULFILMENT, INVENTORY_CATALOGUE, RETURN_ORDER, BILLING_ACCOUNT, VIRTUAL_CATALOGUE, PRODUCT_CATALOGUE, CONTROL_GROUP, PAYMENT, WAVE, CONSIGNMENT, ARTICLE)."),
|
|
49
|
+
entitySubtype: z
|
|
50
|
+
.string()
|
|
51
|
+
.describe("Workflow entity subtype (e.g., HD, DEFAULT, MASTER, BASE, CC, CUSTOMER, REFUND, PAYMENT)."),
|
|
52
|
+
retailerId: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Target retailer ID. Falls back to FLUENT_RETAILER_ID."),
|
|
56
|
+
version: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Specific workflow version to fetch. Omit for latest."),
|
|
60
|
+
outputFile: z
|
|
61
|
+
.string()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Optional file path to save the full workflow JSON. " +
|
|
64
|
+
"When provided, writes workflow to file and returns only summary " +
|
|
65
|
+
"(name, version, status/ruleset counts) — keeps large workflow JSON out of the LLM context. " +
|
|
66
|
+
"Parent directories are created automatically."),
|
|
67
|
+
});
|
|
68
|
+
export const WorkflowListInputSchema = z.object({
|
|
69
|
+
retailerId: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Target retailer ID. Falls back to FLUENT_RETAILER_ID."),
|
|
73
|
+
outputDir: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Optional directory path to download ALL workflows (latest version each) as individual JSON files. " +
|
|
77
|
+
"Each file is named <ENTITY_TYPE>-<ENTITY_SUBTYPE>.json (e.g., ORDER-HD.json). " +
|
|
78
|
+
"Returns only summary metadata — full workflow JSON is NOT included in the response. " +
|
|
79
|
+
"Reads from live server (NOT workflowlog cache). Parent directories are created automatically."),
|
|
80
|
+
});
|
|
43
81
|
export const WorkflowSimulateInputSchema = z.object({
|
|
44
82
|
workflow: z
|
|
45
83
|
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
@@ -64,6 +102,91 @@ export const WorkflowSimulateInputSchema = z.object({
|
|
|
64
102
|
// Tool definitions (JSON Schema for MCP)
|
|
65
103
|
// ---------------------------------------------------------------------------
|
|
66
104
|
export const WORKFLOW_TOOL_DEFINITIONS = [
|
|
105
|
+
{
|
|
106
|
+
name: "workflow.get",
|
|
107
|
+
description: [
|
|
108
|
+
"Fetch a workflow definition by entity type and subtype from the Fluent REST API.",
|
|
109
|
+
"",
|
|
110
|
+
"Calls GET /api/v4.1/workflow/{retailerId}/{entityType}::{entitySubtype}/",
|
|
111
|
+
"Returns the full workflow JSON including statuses, rulesets, rules, and triggers.",
|
|
112
|
+
"",
|
|
113
|
+
"NOTE: This fetches a specific workflow by name, which works even when the list",
|
|
114
|
+
"endpoint returns 401 (insufficient permissions for listing).",
|
|
115
|
+
"",
|
|
116
|
+
"KNOWN ENTITY TYPES:",
|
|
117
|
+
"ORDER, FULFILMENT, INVENTORY_CATALOGUE, RETURN_ORDER, BILLING_ACCOUNT,",
|
|
118
|
+
"VIRTUAL_CATALOGUE, PRODUCT_CATALOGUE, CONTROL_GROUP, PAYMENT, WAVE,",
|
|
119
|
+
"CONSIGNMENT, ARTICLE, LOCATION, NETWORK, CUSTOMER, CREDIT_MEMO",
|
|
120
|
+
"",
|
|
121
|
+
"COMMON SUBTYPES:",
|
|
122
|
+
"HD, CC, DEFAULT, MASTER, BASE, CUSTOMER, REFUND, PAYMENT, HD_WH, HD_MP",
|
|
123
|
+
"",
|
|
124
|
+
"RESPONSE includes: name, version, entityType, entitySubtype, statuses, rulesets,",
|
|
125
|
+
"and a summary with status/ruleset/rule counts.",
|
|
126
|
+
"",
|
|
127
|
+
"Use outputFile to save large workflow JSON to disk instead of returning inline.",
|
|
128
|
+
"Returns only summary (name, version, counts) when outputFile is set.",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
entityType: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Workflow entity type (e.g., ORDER, FULFILMENT, INVENTORY_CATALOGUE)",
|
|
136
|
+
},
|
|
137
|
+
entitySubtype: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Workflow entity subtype (e.g., HD, DEFAULT, MASTER)",
|
|
140
|
+
},
|
|
141
|
+
retailerId: {
|
|
142
|
+
type: "string",
|
|
143
|
+
description: "Target retailer ID.",
|
|
144
|
+
},
|
|
145
|
+
version: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "Specific workflow version. Omit for latest.",
|
|
148
|
+
},
|
|
149
|
+
outputFile: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "File path to save workflow JSON. Returns summary only (not full JSON) when set.",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
required: ["entityType", "entitySubtype"],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "workflow.list",
|
|
160
|
+
description: [
|
|
161
|
+
"List all workflows for a retailer from the Fluent REST API.",
|
|
162
|
+
"",
|
|
163
|
+
"Calls GET /api/v4.1/workflow?retailerId={retailerId}",
|
|
164
|
+
"Returns a list of workflow summaries (name, version, entityType, entitySubtype, status).",
|
|
165
|
+
"",
|
|
166
|
+
"When outputDir is provided, downloads ALL workflows (latest version each) as individual",
|
|
167
|
+
"JSON files to the directory. Each file is named <TYPE>-<SUBTYPE>.json (e.g., ORDER-HD.json).",
|
|
168
|
+
"Returns only summary metadata — full JSON stays on disk, not in the response.",
|
|
169
|
+
"Reads from LIVE server, not the workflowlog cache.",
|
|
170
|
+
"",
|
|
171
|
+
"NOTE: This endpoint may return 401 on some accounts where the user lacks listing",
|
|
172
|
+
"permissions. In that case, use workflow.get to fetch a specific workflow by name.",
|
|
173
|
+
].join("\n"),
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
retailerId: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "Target retailer ID.",
|
|
180
|
+
},
|
|
181
|
+
outputDir: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "Directory to save all workflows as individual JSON files. Returns summary only when set.",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: [],
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
67
190
|
{
|
|
68
191
|
name: "workflow.upload",
|
|
69
192
|
description: [
|
|
@@ -750,3 +873,166 @@ export async function handleWorkflowSimulate(args, _ctx) {
|
|
|
750
873
|
limitations,
|
|
751
874
|
};
|
|
752
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Handle workflow.get tool call.
|
|
878
|
+
* Fetches a specific workflow by entityType::entitySubtype via REST API.
|
|
879
|
+
*/
|
|
880
|
+
export async function handleWorkflowGet(args, ctx) {
|
|
881
|
+
const parsed = WorkflowGetInputSchema.parse(args);
|
|
882
|
+
const retailerId = parsed.retailerId ?? ctx.config.retailerId;
|
|
883
|
+
if (!retailerId) {
|
|
884
|
+
throw new ToolError("VALIDATION_ERROR", "retailerId is required. Set FLUENT_RETAILER_ID or pass retailerId.");
|
|
885
|
+
}
|
|
886
|
+
const client = requireWorkflowClient(ctx);
|
|
887
|
+
const workflowName = `${parsed.entityType}::${parsed.entitySubtype}`;
|
|
888
|
+
const response = await client.getWorkflow(retailerId, parsed.entityType, parsed.entitySubtype, parsed.version);
|
|
889
|
+
// Extract summary info from the workflow
|
|
890
|
+
const wf = response;
|
|
891
|
+
if (!wf || typeof wf !== "object") {
|
|
892
|
+
throw new ToolError("VALIDATION_ERROR", `No workflow found for ${workflowName} on retailer ${retailerId}.`);
|
|
893
|
+
}
|
|
894
|
+
const statuses = (wf.statuses ?? wf.subStatuses);
|
|
895
|
+
const rulesets = wf.rulesets;
|
|
896
|
+
const totalRules = (rulesets ?? []).reduce((sum, rs) => {
|
|
897
|
+
const rules = rs.rules;
|
|
898
|
+
return sum + (rules?.length ?? 0);
|
|
899
|
+
}, 0);
|
|
900
|
+
const summary = {
|
|
901
|
+
statuses: statuses?.length ?? 0,
|
|
902
|
+
rulesets: rulesets?.length ?? 0,
|
|
903
|
+
totalRules,
|
|
904
|
+
};
|
|
905
|
+
// If outputFile is provided, write workflow to file and return summary only
|
|
906
|
+
if (parsed.outputFile) {
|
|
907
|
+
const fileContent = JSON.stringify(wf, null, 2);
|
|
908
|
+
const dir = path.dirname(parsed.outputFile);
|
|
909
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
910
|
+
fs.writeFileSync(parsed.outputFile, fileContent, "utf-8");
|
|
911
|
+
return {
|
|
912
|
+
ok: true,
|
|
913
|
+
workflowName: wf.name ?? workflowName,
|
|
914
|
+
version: wf.version,
|
|
915
|
+
entityType: wf.entityType ?? parsed.entityType,
|
|
916
|
+
entitySubtype: wf.entitySubtype ?? parsed.entitySubtype,
|
|
917
|
+
retailerId,
|
|
918
|
+
summary,
|
|
919
|
+
savedTo: parsed.outputFile,
|
|
920
|
+
sizeBytes: Buffer.byteLength(fileContent, "utf-8"),
|
|
921
|
+
message: `Workflow saved to ${parsed.outputFile} (${Buffer.byteLength(fileContent, "utf-8")} bytes). Full JSON NOT included in response.`,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
ok: true,
|
|
926
|
+
workflowName: wf.name ?? workflowName,
|
|
927
|
+
version: wf.version,
|
|
928
|
+
entityType: wf.entityType ?? parsed.entityType,
|
|
929
|
+
entitySubtype: wf.entitySubtype ?? parsed.entitySubtype,
|
|
930
|
+
retailerId,
|
|
931
|
+
summary,
|
|
932
|
+
workflow: wf,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Handle workflow.list tool call.
|
|
937
|
+
* Lists all workflows for a retailer via REST API.
|
|
938
|
+
*/
|
|
939
|
+
export async function handleWorkflowList(args, ctx) {
|
|
940
|
+
const parsed = WorkflowListInputSchema.parse(args);
|
|
941
|
+
const retailerId = parsed.retailerId ?? ctx.config.retailerId;
|
|
942
|
+
if (!retailerId) {
|
|
943
|
+
throw new ToolError("VALIDATION_ERROR", "retailerId is required. Set FLUENT_RETAILER_ID or pass retailerId.");
|
|
944
|
+
}
|
|
945
|
+
const client = requireWorkflowClient(ctx);
|
|
946
|
+
const response = await client.listWorkflows(retailerId);
|
|
947
|
+
// Response can be: bare array, paginated { results: [...] }, or unwrapped from { data: ... }
|
|
948
|
+
let workflows;
|
|
949
|
+
if (Array.isArray(response)) {
|
|
950
|
+
workflows = response;
|
|
951
|
+
}
|
|
952
|
+
else if (response && typeof response === "object" && "results" in response) {
|
|
953
|
+
workflows = response.results;
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
workflows = [];
|
|
957
|
+
}
|
|
958
|
+
// Deduplicate: group by name, keep only latest version
|
|
959
|
+
const latestByName = new Map();
|
|
960
|
+
for (const wf of workflows) {
|
|
961
|
+
const w = wf;
|
|
962
|
+
const name = w.name;
|
|
963
|
+
if (!name)
|
|
964
|
+
continue;
|
|
965
|
+
const existing = latestByName.get(name);
|
|
966
|
+
if (!existing || Number(w.version) > Number(existing.version)) {
|
|
967
|
+
latestByName.set(name, w);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const latest = [...latestByName.values()].map((w) => ({
|
|
971
|
+
name: w.name,
|
|
972
|
+
version: w.version,
|
|
973
|
+
entityType: w.entityType,
|
|
974
|
+
entitySubtype: w.entitySubtype,
|
|
975
|
+
status: w.status,
|
|
976
|
+
createdOn: w.createdOn,
|
|
977
|
+
}));
|
|
978
|
+
// If outputDir is provided, download each workflow's full JSON to individual files
|
|
979
|
+
if (parsed.outputDir && latest.length > 0) {
|
|
980
|
+
fs.mkdirSync(parsed.outputDir, { recursive: true });
|
|
981
|
+
const savedFiles = [];
|
|
982
|
+
const errors = [];
|
|
983
|
+
for (const wf of latest) {
|
|
984
|
+
const entityType = wf.entityType;
|
|
985
|
+
const entitySubtype = wf.entitySubtype;
|
|
986
|
+
if (!entityType || !entitySubtype)
|
|
987
|
+
continue;
|
|
988
|
+
try {
|
|
989
|
+
const fullWf = await client.getWorkflow(retailerId, entityType, entitySubtype);
|
|
990
|
+
if (fullWf && typeof fullWf === "object") {
|
|
991
|
+
const fileContent = JSON.stringify(fullWf, null, 2);
|
|
992
|
+
const fileName = `${entityType}-${entitySubtype}.json`;
|
|
993
|
+
const filePath = path.join(parsed.outputDir, fileName);
|
|
994
|
+
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
995
|
+
savedFiles.push({
|
|
996
|
+
name: wf.name,
|
|
997
|
+
file: filePath,
|
|
998
|
+
sizeBytes: Buffer.byteLength(fileContent, "utf-8"),
|
|
999
|
+
version: wf.version,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
errors.push({
|
|
1005
|
+
name: `${entityType}::${entitySubtype}`,
|
|
1006
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const totalBytes = savedFiles.reduce((s, f) => s + f.sizeBytes, 0);
|
|
1011
|
+
return {
|
|
1012
|
+
ok: true,
|
|
1013
|
+
retailerId,
|
|
1014
|
+
totalVersions: workflows.length,
|
|
1015
|
+
uniqueWorkflows: latest.length,
|
|
1016
|
+
savedTo: parsed.outputDir,
|
|
1017
|
+
filesWritten: savedFiles.length,
|
|
1018
|
+
totalSizeBytes: totalBytes,
|
|
1019
|
+
files: savedFiles,
|
|
1020
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
1021
|
+
message: `${savedFiles.length} workflow(s) saved to ${parsed.outputDir}/ (${(totalBytes / 1024).toFixed(1)} KB total). ` +
|
|
1022
|
+
`Full JSON NOT included in response.` +
|
|
1023
|
+
(errors.length > 0
|
|
1024
|
+
? ` ${errors.length} workflow(s) failed to download.`
|
|
1025
|
+
: ""),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
ok: true,
|
|
1030
|
+
retailerId,
|
|
1031
|
+
totalVersions: workflows.length,
|
|
1032
|
+
uniqueWorkflows: latest.length,
|
|
1033
|
+
workflows: latest,
|
|
1034
|
+
note: workflows.length === 0
|
|
1035
|
+
? "No workflows found. The list endpoint may return 401 on some accounts — try workflow.get with a specific entityType and entitySubtype instead."
|
|
1036
|
+
: undefined,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
package/docs/RUNBOOK.md
CHANGED
|
@@ -92,8 +92,11 @@ Run tools in this order:
|
|
|
92
92
|
7. `event.get` using ID returned by send
|
|
93
93
|
8. `graphql.query` simple read
|
|
94
94
|
9. `entity.get` — verify entity lookup works (e.g., look up a known order by ref)
|
|
95
|
-
10. `
|
|
96
|
-
11. `
|
|
95
|
+
10. `setting.get` with `name: "fc.mystique.manifest.%"` — verify settings read access and wildcard support
|
|
96
|
+
11. `workflow.list` — verify REST workflow API access (list all workflows for a retailer)
|
|
97
|
+
12. `workflow.get` with `entityType: "ORDER", entitySubtype: "HD"` — verify specific workflow fetch
|
|
98
|
+
13. `workflow.transitions` (optional) — verify User Action API access for a known trigger
|
|
99
|
+
14. `graphql.queryAll` — verify auto-pagination and run-level timeout
|
|
97
100
|
12. `graphql.introspect` with `listMutations: true` — verify schema access
|
|
98
101
|
13. `environment.discover` with `include: ["retailer", "locations"]` — verify environment snapshot
|
|
99
102
|
14. `environment.validate` with `checks: ["auth", "retailer", "locations"]` — verify pre-flight checks
|
package/docs/TOOL_REFERENCE.md
CHANGED
|
@@ -462,6 +462,7 @@ Calls `POST /api/v4.1/transition` to discover what events can be fired, what att
|
|
|
462
462
|
- **Required**: `triggers` (array), each trigger requires `retailerId`
|
|
463
463
|
- **Optional per trigger**: `type`, `subtype`, `status`, `module`, `flexType`, `flexVersion`, `name`
|
|
464
464
|
- **Retailer behavior**: `retailerId` is required per trigger. Falls back to `FLUENT_RETAILER_ID` when omitted.
|
|
465
|
+
- **flexType auto-derive**: When `flexType` is not provided but `type` and `subtype` are, the tool auto-derives `flexType` as `{type}::{subtype}` (e.g., `CREDIT_MEMO::APPEASEMENT`). This avoids the silent empty-response problem where the Transition API returns `{"response":[]}` when `flexType` is missing.
|
|
465
466
|
|
|
466
467
|
### Use cases
|
|
467
468
|
|
|
@@ -1309,6 +1310,62 @@ Response:
|
|
|
1309
1310
|
|
|
1310
1311
|
---
|
|
1311
1312
|
|
|
1313
|
+
## `workflow.get`
|
|
1314
|
+
|
|
1315
|
+
Fetch a specific workflow definition by entity type and subtype via REST API.
|
|
1316
|
+
|
|
1317
|
+
Uses `GET /api/v4.1/workflow/{retailerId}/{entityType}::{entitySubtype}/` — works even when the list endpoint returns 401 (permission-restricted accounts).
|
|
1318
|
+
|
|
1319
|
+
- **Required**:
|
|
1320
|
+
- `entityType`: workflow entity type (e.g., `ORDER`, `FULFILMENT`, `INVENTORY_CATALOGUE`)
|
|
1321
|
+
- `entitySubtype`: workflow entity subtype (e.g., `HD`, `DEFAULT`, `MASTER`)
|
|
1322
|
+
- **Optional**:
|
|
1323
|
+
- `retailerId`: target retailer ID (falls back to `FLUENT_RETAILER_ID`)
|
|
1324
|
+
- `version`: specific workflow version to fetch (omit for latest)
|
|
1325
|
+
- `outputFile`: file path to save workflow JSON. When set, writes full workflow to file and returns only summary (name, version, status/ruleset/rule counts) — keeps large workflow JSON out of the LLM context. Parent directories are created automatically.
|
|
1326
|
+
|
|
1327
|
+
**Response** includes a summary (status count, ruleset count, total rules) plus the full workflow JSON (unless `outputFile` is set).
|
|
1328
|
+
|
|
1329
|
+
**Known entity types:** ORDER, FULFILMENT, FULFILMENT_CHOICE, ARTICLE, RETURN_ORDER, WAVE, CONSIGNMENT, INVENTORY_CATALOGUE, INVENTORY_POSITION, VIRTUAL_CATALOGUE, LOCATION, PRODUCT, CUSTOMER, CARRIER, NETWORK, CREDIT_MEMO.
|
|
1330
|
+
|
|
1331
|
+
**Example:**
|
|
1332
|
+
|
|
1333
|
+
```json
|
|
1334
|
+
{
|
|
1335
|
+
"entityType": "INVENTORY_CATALOGUE",
|
|
1336
|
+
"entitySubtype": "DEFAULT",
|
|
1337
|
+
"retailerId": "34"
|
|
1338
|
+
}
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
**Note:** Fluent returns HTTP 200 with an empty body for nonexistent workflows — the handler checks for meaningful content (presence of `name` field) and returns a clear "not found" message.
|
|
1342
|
+
|
|
1343
|
+
---
|
|
1344
|
+
|
|
1345
|
+
## `workflow.list`
|
|
1346
|
+
|
|
1347
|
+
List all workflows for a retailer via REST API.
|
|
1348
|
+
|
|
1349
|
+
Uses `GET /api/v4.1/workflow?retailerId={id}&count=200`. Response is deduplicated to show only the latest version of each workflow. Reads from **live server**, NOT the workflowlog cache.
|
|
1350
|
+
|
|
1351
|
+
- **Optional**:
|
|
1352
|
+
- `retailerId`: target retailer ID (falls back to `FLUENT_RETAILER_ID`)
|
|
1353
|
+
- `outputDir`: directory path to download ALL workflows (latest version each) as individual JSON files. Each file is named `<TYPE>-<SUBTYPE>.json`. Returns only summary metadata when set — full JSON stays on disk. Parent directories are created automatically.
|
|
1354
|
+
|
|
1355
|
+
**Response** includes per-workflow summary (name, version, status count, ruleset count) and totals.
|
|
1356
|
+
|
|
1357
|
+
**Example:**
|
|
1358
|
+
|
|
1359
|
+
```json
|
|
1360
|
+
{
|
|
1361
|
+
"retailerId": "35"
|
|
1362
|
+
}
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
**Note:** Some accounts restrict this endpoint (401 "Incorrect retailer"). In that case, use `workflow.get` with a specific entityType + entitySubtype instead.
|
|
1366
|
+
|
|
1367
|
+
---
|
|
1368
|
+
|
|
1312
1369
|
## `workflow.upload`
|
|
1313
1370
|
|
|
1314
1371
|
Deploy a workflow JSON definition to the Fluent environment.
|
|
@@ -1491,6 +1548,57 @@ Response:
|
|
|
1491
1548
|
|
|
1492
1549
|
---
|
|
1493
1550
|
|
|
1551
|
+
## `setting.get`
|
|
1552
|
+
|
|
1553
|
+
Fetch one or more Fluent Commerce settings by name (supports `%` wildcards).
|
|
1554
|
+
|
|
1555
|
+
Uses the GraphQL `settings(name:)` query. Returns metadata inline (name, context, contextId, valueType). For large settings (manifests, JSON > 4KB), use `outputFile` to save content to a local file and keep it out of the LLM context.
|
|
1556
|
+
|
|
1557
|
+
- **Required**:
|
|
1558
|
+
- `name`: setting name or pattern. Supports `%` wildcards (e.g., `fc.mystique.manifest.%` for all manifest settings)
|
|
1559
|
+
- **Optional**:
|
|
1560
|
+
- `context`: filter by context — `ACCOUNT`, `RETAILER`, etc.
|
|
1561
|
+
- `contextId`: context ID. Falls back to `FLUENT_RETAILER_ID` for `RETAILER` context, `0` for `ACCOUNT`.
|
|
1562
|
+
- `outputFile`: file path to save the setting value/lobValue. When provided, writes content to file and returns only metadata (name, context, valueType, size) — keeps large manifests out of the LLM context. Parent directories are created automatically.
|
|
1563
|
+
- `first`: max results (default: 10, max: 100)
|
|
1564
|
+
|
|
1565
|
+
**Behavior with `outputFile`:**
|
|
1566
|
+
|
|
1567
|
+
- **Single match** + `outputFile` → writes lobValue (or value) to the file, returns metadata + file path + byte size (NOT the content)
|
|
1568
|
+
- **Multiple matches** + `outputFile` → writes each setting to a separate file in the directory (named `<setting-name>.json`), returns per-file metadata
|
|
1569
|
+
- **No `outputFile`** → returns full setting data inline (may be large for manifests)
|
|
1570
|
+
- JSON content is automatically pretty-printed when saved to file
|
|
1571
|
+
|
|
1572
|
+
**Example** — fetch a specific manifest setting to disk:
|
|
1573
|
+
|
|
1574
|
+
```json
|
|
1575
|
+
{
|
|
1576
|
+
"name": "fc.mystique.manifest.oms",
|
|
1577
|
+
"context": "ACCOUNT",
|
|
1578
|
+
"outputFile": "./accounts/SAGIRISH/manifests/backups/fc.mystique.manifest.oms.json"
|
|
1579
|
+
}
|
|
1580
|
+
```
|
|
1581
|
+
|
|
1582
|
+
**Example** — list all manifest settings (wildcard):
|
|
1583
|
+
|
|
1584
|
+
```json
|
|
1585
|
+
{
|
|
1586
|
+
"name": "fc.mystique.manifest.%",
|
|
1587
|
+
"context": "ACCOUNT"
|
|
1588
|
+
}
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
**Example** — download all manifests to directory:
|
|
1592
|
+
|
|
1593
|
+
```json
|
|
1594
|
+
{
|
|
1595
|
+
"name": "fc.mystique.manifest.%",
|
|
1596
|
+
"outputFile": "./accounts/SAGIRISH/manifests/backups/"
|
|
1597
|
+
}
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
---
|
|
1601
|
+
|
|
1494
1602
|
## `setting.upsert`
|
|
1495
1603
|
|
|
1496
1604
|
Create or update a Fluent Commerce setting with upsert semantics.
|
|
@@ -1504,12 +1612,14 @@ Queries existing settings by `name` + `context` + `contextId` first. Creates if
|
|
|
1504
1612
|
- **Optional**:
|
|
1505
1613
|
- `lobValue`: large object value for JSON payloads > 4KB (mutually exclusive with `value`)
|
|
1506
1614
|
- `contextId`: context ID (e.g., retailer ID). Defaults to `FLUENT_RETAILER_ID` for `RETAILER` context.
|
|
1615
|
+
- `valueType`: explicit value type — `"STRING"`, `"LOB"`, or `"JSON"`. **Use `"JSON"` for Mystique manifest settings** (`fc.mystique.*`). Defaults to `"LOB"` when `lobValue` is provided, `"STRING"` when `value` is provided.
|
|
1507
1616
|
|
|
1508
1617
|
**Key gotchas:**
|
|
1509
1618
|
|
|
1510
1619
|
- `context` is a plain String (`"RETAILER"`), NOT an object
|
|
1511
1620
|
- `contextId` is a separate Int field
|
|
1512
1621
|
- For large JSON values (> 4KB), use `lobValue` instead of `value`
|
|
1622
|
+
- **For Mystique manifest settings, set `valueType: "JSON"`** — without it, defaults to `"LOB"` which breaks manifest parsing. The tool emits a `warning` field in the response when a `fc.mystique.manifest.*` name is used without `valueType: "JSON"`.
|
|
1513
1623
|
- Returns `created: true/false` for audit trail
|
|
1514
1624
|
|
|
1515
1625
|
**Example** — create a webhook URL setting:
|
|
@@ -1560,7 +1670,7 @@ Batch create or update multiple settings in one call.
|
|
|
1560
1670
|
Processes up to 50 settings sequentially with individual error handling. Each setting is an independent upsert — failures on one setting don't block others.
|
|
1561
1671
|
|
|
1562
1672
|
- **Required**:
|
|
1563
|
-
- `settings`: array of setting objects (min 1, max 50), each with `name`, `value`, `context`, and optional `lobValue` and `
|
|
1673
|
+
- `settings`: array of setting objects (min 1, max 50), each with `name`, `value`, `context`, and optional `lobValue`, `contextId`, and `valueType` (`"STRING"`, `"LOB"`, or `"JSON"`)
|
|
1564
1674
|
|
|
1565
1675
|
**Example** — bulk create 3 settings:
|
|
1566
1676
|
|
|
@@ -1763,11 +1873,11 @@ Response (fail after timeout):
|
|
|
1763
1873
|
|
|
1764
1874
|
| Category | Tools | Retry behavior |
|
|
1765
1875
|
|------|----------|-------------|
|
|
1766
|
-
| Read operations | `event.get`, `event.list`, `event.flowInspect`, `metrics.query`, `metrics.healthCheck`, `metrics.sloReport`, `metrics.labelCatalog`, `metrics.topEvents`, `workflow.transitions`, `graphql.query` (query), `graphql.queryAll`, `batch.status`, `batch.batchStatus`, `batch.results`, `graphql.introspect`, `connection.test`, `entity.get`, `environment.discover`, `environment.validate`, `test.assert`, `plugin.list` | retry + backoff |
|
|
1876
|
+
| Read operations | `event.get`, `event.list`, `event.flowInspect`, `metrics.query`, `metrics.healthCheck`, `metrics.sloReport`, `metrics.labelCatalog`, `metrics.topEvents`, `workflow.transitions`, `workflow.get`, `workflow.list`, `graphql.query` (query), `graphql.queryAll`, `batch.status`, `batch.batchStatus`, `batch.results`, `graphql.introspect`, `connection.test`, `entity.get`, `setting.get`, `environment.discover`, `environment.validate`, `test.assert`, `plugin.list` | retry + backoff |
|
|
1767
1877
|
| Write operations | `event.send`, `batch.create`, `batch.send`, `graphql.query` (mutation), `graphql.batchMutate`, `entity.create`, `entity.update`, `workflow.upload`, `setting.upsert`, `setting.bulkUpsert` | timeout-only (no automatic retry) |
|
|
1768
1878
|
| Local validation / no API | `config.validate`, `health.ping`, `event.build`, `webhook.validate`, `workflow.diff`, `workflow.simulate` | not applicable |
|
|
1769
1879
|
|
|
1770
|
-
## Tool inventory summary (
|
|
1880
|
+
## Tool inventory summary (39 tools)
|
|
1771
1881
|
|
|
1772
1882
|
| Tool | Category | Requires SDK | Retry | Description |
|
|
1773
1883
|
|------|----------|:------------:|:-----:|-------------|
|
|
@@ -1799,9 +1909,12 @@ Response (fail after timeout):
|
|
|
1799
1909
|
| `entity.create` | Entity | Yes | No | Type-safe entity creation with field validation and gotcha knowledge (12 entity types) |
|
|
1800
1910
|
| `entity.update` | Entity | Yes | No | Status-aware entity updates with optional transition validation |
|
|
1801
1911
|
| `entity.get` | Entity | Yes | Yes | Unified entity lookup by ID or ref with optional edge inclusion |
|
|
1912
|
+
| `workflow.get` | Workflow Mgmt | Yes | Yes | Fetch specific workflow by entityType + entitySubtype via REST API |
|
|
1913
|
+
| `workflow.list` | Workflow Mgmt | Yes | Yes | List all workflows for a retailer (deduplicated to latest versions) |
|
|
1802
1914
|
| `workflow.upload` | Workflow Mgmt | Yes | No | Deploy workflow JSON via REST API with structure validation |
|
|
1803
1915
|
| `workflow.diff` | Workflow Mgmt | No | — | Compare two workflows — rulesets, statuses, risk assessment, mermaid output |
|
|
1804
1916
|
| `workflow.simulate` | Workflow Mgmt | No | — | Static prediction of event outcomes from workflow JSON |
|
|
1917
|
+
| `setting.get` | Settings | Yes | Yes | Fetch settings by name (wildcards supported), optionally save to local file |
|
|
1805
1918
|
| `setting.upsert` | Settings | Yes | No | Create or update a setting with upsert semantics |
|
|
1806
1919
|
| `setting.bulkUpsert` | Settings | Yes | No | Batch create/update up to 50 settings with per-setting error handling |
|
|
1807
1920
|
| `environment.discover` | Environment | Yes | Yes | Full environment snapshot — retailer, locations, networks, catalogues, settings, modules |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluentcommerce/fluent-mcp-extn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP (Model Context Protocol) extension server for Fluent Commerce. Exposes event dispatch, transition actions, GraphQL execution, Prometheus metrics, batch ingestion, and webhook validation as MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|