@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 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 (36)](#tools-36)
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
- Pass all credentials directly in env vars:
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 (36)
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 plugin repos and JAR files
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
  ```
@@ -286,6 +286,7 @@ const ENTITY_REGISTRY = {
286
286
  'context is a plain String ("RETAILER"), NOT { contextType: "RETAILER" }',
287
287
  "contextId is a separate Int field, not combined with context",
288
288
  "For large JSON values, use lobValue instead of value (lobType: LOB)",
289
+ 'For Mystique manifest settings (fc.mystique.*), set valueType:"JSON" — LOB type breaks manifest parsing silently',
289
290
  ],
290
291
  hasRef: false,
291
292
  retailerScoped: false,
@@ -140,6 +140,36 @@ export class FluentClientAdapter {
140
140
  return FluentClientAdapter.unwrapResponse(response);
141
141
  });
142
142
  }
143
+ /**
144
+ * Fetch a workflow definition by name from the REST API.
145
+ * GET /api/v4.1/workflow/{retailerId}/{entityType}::{entitySubtype}/
146
+ * Optionally fetch a specific version:
147
+ * GET /api/v4.1/workflow/{retailerId}/{entityType}::{entitySubtype}/{version}
148
+ * Read-only — safe to retry.
149
+ */
150
+ async getWorkflow(retailerId, entityType, entitySubtype, version) {
151
+ const requestClient = this.requireRequestClient("Workflow REST API");
152
+ const workflowName = `${entityType}::${entitySubtype}`;
153
+ const path = version
154
+ ? `/api/v4.1/workflow/${retailerId}/${workflowName}/${version}`
155
+ : `/api/v4.1/workflow/${retailerId}/${workflowName}/`;
156
+ return this.execute("getWorkflow", async () => {
157
+ const response = await requestClient.request(path, { method: "GET" });
158
+ return FluentClientAdapter.unwrapResponse(response);
159
+ });
160
+ }
161
+ /**
162
+ * List all workflows for a retailer from the REST API.
163
+ * GET /api/v4.1/workflow?retailerId={retailerId}
164
+ * Read-only — safe to retry.
165
+ */
166
+ async listWorkflows(retailerId) {
167
+ const requestClient = this.requireRequestClient("Workflow REST API");
168
+ return this.execute("listWorkflows", async () => {
169
+ const response = await requestClient.request(`/api/v4.1/workflow?retailerId=${retailerId}&count=200`, { method: "GET" });
170
+ return FluentClientAdapter.unwrapResponse(response);
171
+ });
172
+ }
143
173
  /**
144
174
  * Query Prometheus metrics via GraphQL metricInstant / metricRange queries.
145
175
  * The Fluent platform does NOT expose raw Prometheus REST endpoints
@@ -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
- return {
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
- return {
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
  });