@fluentcommerce/fluent-mcp-extn 0.2.1 → 0.3.1
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 +285 -4
- package/dist/tools.js +48 -7
- package/dist/workflow-tools.js +286 -0
- package/docs/E2E_TESTING.md +12 -12
- 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,155 @@ 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 raw = setting.lobValue ?? setting.value ?? "";
|
|
428
|
+
// lobValue may be a parsed object (valueType: JSON) or a string — normalize to string
|
|
429
|
+
const content = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
|
|
430
|
+
// Try to pretty-print JSON content (if it's a JSON string, reformat)
|
|
431
|
+
let fileContent = content;
|
|
432
|
+
try {
|
|
433
|
+
const obj = JSON.parse(content);
|
|
434
|
+
fileContent = JSON.stringify(obj, null, 2);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Not JSON — write as-is
|
|
438
|
+
}
|
|
439
|
+
// Ensure parent directory exists
|
|
440
|
+
const dir = path.dirname(parsed.outputFile);
|
|
441
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
442
|
+
fs.writeFileSync(parsed.outputFile, fileContent, "utf-8");
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
count: 1,
|
|
446
|
+
savedTo: parsed.outputFile,
|
|
447
|
+
sizeBytes: Buffer.byteLength(fileContent, "utf-8"),
|
|
448
|
+
setting: {
|
|
449
|
+
id: setting.id,
|
|
450
|
+
name: setting.name,
|
|
451
|
+
context: setting.context,
|
|
452
|
+
contextId: setting.contextId,
|
|
453
|
+
valueType: setting.valueType,
|
|
454
|
+
},
|
|
455
|
+
message: `Setting "${setting.name}" saved to ${parsed.outputFile} (${Buffer.byteLength(fileContent, "utf-8")} bytes). Content NOT included in response.`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// If outputFile provided but multiple settings match, warn
|
|
459
|
+
if (parsed.outputFile && settings.length > 1) {
|
|
460
|
+
// Write each setting to a separate file in the outputFile directory
|
|
461
|
+
const outputDir = parsed.outputFile.endsWith(".json")
|
|
462
|
+
? path.dirname(parsed.outputFile)
|
|
463
|
+
: parsed.outputFile;
|
|
464
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
465
|
+
const savedFiles = [];
|
|
466
|
+
for (const setting of settings) {
|
|
467
|
+
const raw = setting.lobValue ?? setting.value ?? "";
|
|
468
|
+
// lobValue may be a parsed object (valueType: JSON) or a string — normalize to string
|
|
469
|
+
const content = typeof raw === "string" ? raw : JSON.stringify(raw, null, 2);
|
|
470
|
+
let fileContent = content;
|
|
471
|
+
try {
|
|
472
|
+
const p = JSON.parse(content);
|
|
473
|
+
fileContent = JSON.stringify(p, null, 2);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Not JSON — write as-is
|
|
477
|
+
}
|
|
478
|
+
const safeName = setting.name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
479
|
+
const filePath = path.join(outputDir, `${safeName}.json`);
|
|
480
|
+
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
481
|
+
savedFiles.push({
|
|
482
|
+
name: setting.name,
|
|
483
|
+
file: filePath,
|
|
484
|
+
sizeBytes: Buffer.byteLength(fileContent, "utf-8"),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
ok: true,
|
|
489
|
+
count: settings.length,
|
|
490
|
+
savedTo: outputDir,
|
|
491
|
+
files: savedFiles,
|
|
492
|
+
settings: settings.map((s) => ({
|
|
493
|
+
id: s.id,
|
|
494
|
+
name: s.name,
|
|
495
|
+
context: s.context,
|
|
496
|
+
contextId: s.contextId,
|
|
497
|
+
valueType: s.valueType,
|
|
498
|
+
})),
|
|
499
|
+
message: `${settings.length} settings saved to ${outputDir}/. Content NOT included in response.`,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// No outputFile — return everything inline
|
|
503
|
+
return {
|
|
504
|
+
ok: true,
|
|
505
|
+
count: settings.length,
|
|
506
|
+
settings: settings.map((s) => ({
|
|
507
|
+
id: s.id,
|
|
508
|
+
name: s.name,
|
|
509
|
+
value: s.value,
|
|
510
|
+
lobValue: s.lobValue,
|
|
511
|
+
context: s.context,
|
|
512
|
+
contextId: s.contextId,
|
|
513
|
+
valueType: s.valueType,
|
|
514
|
+
})),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
253
517
|
/**
|
|
254
518
|
* Handle setting.upsert tool call.
|
|
255
519
|
*/
|
|
@@ -259,37 +523,52 @@ export async function handleSettingUpsert(args, ctx) {
|
|
|
259
523
|
const contextId = resolveContextId(parsed.context, parsed.contextId, ctx.config);
|
|
260
524
|
// Check if setting already exists
|
|
261
525
|
const existing = await findExistingSetting(client, parsed.name, parsed.context, contextId);
|
|
526
|
+
// Warn when manifest setting is used without explicit valueType: "JSON"
|
|
527
|
+
const isManifestSetting = parsed.name.startsWith("fc.mystique.manifest.");
|
|
528
|
+
const manifestWarning = isManifestSetting && parsed.valueType !== "JSON"
|
|
529
|
+
? 'Manifest setting detected without valueType:"JSON". ' +
|
|
530
|
+
"Default LOB type will break Mystique manifest parsing. " +
|
|
531
|
+
'Set valueType:"JSON" for fc.mystique.* settings.'
|
|
532
|
+
: undefined;
|
|
262
533
|
if (existing) {
|
|
263
534
|
// Update
|
|
264
535
|
const setting = await updateSetting(client, {
|
|
265
536
|
id: existing.id,
|
|
266
537
|
value: parsed.value,
|
|
267
538
|
lobValue: parsed.lobValue,
|
|
539
|
+
valueType: parsed.valueType,
|
|
268
540
|
context: parsed.context,
|
|
269
541
|
contextId,
|
|
270
542
|
});
|
|
271
|
-
|
|
543
|
+
const result = {
|
|
272
544
|
ok: true,
|
|
273
545
|
created: false,
|
|
274
546
|
updated: true,
|
|
275
547
|
setting,
|
|
276
548
|
previousValue: existing.value ?? existing.lobValue,
|
|
277
549
|
};
|
|
550
|
+
if (manifestWarning)
|
|
551
|
+
result.warning = manifestWarning;
|
|
552
|
+
return result;
|
|
278
553
|
}
|
|
279
554
|
// Create
|
|
280
555
|
const setting = await createSetting(client, {
|
|
281
556
|
name: parsed.name,
|
|
282
557
|
value: parsed.value,
|
|
283
558
|
lobValue: parsed.lobValue,
|
|
559
|
+
valueType: parsed.valueType,
|
|
284
560
|
context: parsed.context,
|
|
285
561
|
contextId,
|
|
286
562
|
});
|
|
287
|
-
|
|
563
|
+
const result = {
|
|
288
564
|
ok: true,
|
|
289
565
|
created: true,
|
|
290
566
|
updated: false,
|
|
291
567
|
setting,
|
|
292
568
|
};
|
|
569
|
+
if (manifestWarning)
|
|
570
|
+
result.warning = manifestWarning;
|
|
571
|
+
return result;
|
|
293
572
|
}
|
|
294
573
|
/**
|
|
295
574
|
* Handle setting.bulkUpsert tool call.
|
|
@@ -310,6 +589,7 @@ export async function handleSettingBulkUpsert(args, ctx) {
|
|
|
310
589
|
id: existing.id,
|
|
311
590
|
value: setting.value,
|
|
312
591
|
lobValue: setting.lobValue,
|
|
592
|
+
valueType: setting.valueType,
|
|
313
593
|
context: setting.context,
|
|
314
594
|
contextId,
|
|
315
595
|
});
|
|
@@ -321,6 +601,7 @@ export async function handleSettingBulkUpsert(args, ctx) {
|
|
|
321
601
|
name: setting.name,
|
|
322
602
|
value: setting.value,
|
|
323
603
|
lobValue: setting.lobValue,
|
|
604
|
+
valueType: setting.valueType,
|
|
324
605
|
context: setting.context,
|
|
325
606
|
contextId,
|
|
326
607
|
});
|