@fluentcommerce/fluent-mcp-extn 0.1.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.
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Settings management tools: setting.upsert, setting.bulkUpsert
3
+ *
4
+ * Closes the configuration loop — agents can create and update
5
+ * settings that workflows depend on (webhook URLs, feature flags, thresholds).
6
+ */
7
+ import { z } from "zod";
8
+ import { ToolError } from "./errors.js";
9
+ // ---------------------------------------------------------------------------
10
+ // Input schemas
11
+ // ---------------------------------------------------------------------------
12
+ const SettingInputSchema = z.object({
13
+ name: z.string().min(1).describe("Setting key/name"),
14
+ value: z.string().describe("Setting value (for small values). Mutually exclusive with lobValue."),
15
+ lobValue: z
16
+ .string()
17
+ .optional()
18
+ .describe("Large object value (for JSON payloads > 4KB). Mutually exclusive with value."),
19
+ context: z
20
+ .string()
21
+ .min(1)
22
+ .describe('Setting scope context: "RETAILER", "ACCOUNT", "LOCATION", "NETWORK", "AGENT", "CUSTOMER"'),
23
+ contextId: z
24
+ .number()
25
+ .int()
26
+ .optional()
27
+ .describe("Context ID (e.g., retailer ID). Falls back to FLUENT_RETAILER_ID for RETAILER context."),
28
+ });
29
+ export const SettingUpsertInputSchema = SettingInputSchema;
30
+ export const SettingBulkUpsertInputSchema = z.object({
31
+ settings: z
32
+ .array(SettingInputSchema)
33
+ .min(1)
34
+ .max(50)
35
+ .describe("Array of settings to create or update (max 50)"),
36
+ });
37
+ // ---------------------------------------------------------------------------
38
+ // Tool definitions (JSON Schema for MCP)
39
+ // ---------------------------------------------------------------------------
40
+ export const SETTING_TOOL_DEFINITIONS = [
41
+ {
42
+ name: "setting.upsert",
43
+ description: [
44
+ "Create or update a Fluent Commerce setting.",
45
+ "",
46
+ "Upsert semantics: creates if missing, updates if exists.",
47
+ "Queries existing settings by name + context + contextId first.",
48
+ "",
49
+ "KEY GOTCHAS:",
50
+ '- context is a plain String ("RETAILER"), NOT an object',
51
+ "- contextId is a separate Int field",
52
+ "- For large JSON values (>4KB), use lobValue instead of value",
53
+ "- Returns created: true/false for audit trail",
54
+ "",
55
+ "CONTEXTS: RETAILER, ACCOUNT, LOCATION, NETWORK, AGENT, CUSTOMER",
56
+ ].join("\n"),
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ name: {
61
+ type: "string",
62
+ description: "Setting key/name",
63
+ },
64
+ value: {
65
+ type: "string",
66
+ description: "Setting value (for small values)",
67
+ },
68
+ lobValue: {
69
+ type: "string",
70
+ description: "Large object value (for JSON payloads > 4KB)",
71
+ },
72
+ context: {
73
+ type: "string",
74
+ description: 'Setting scope: RETAILER, ACCOUNT, LOCATION, NETWORK, AGENT, CUSTOMER',
75
+ },
76
+ contextId: {
77
+ type: "integer",
78
+ description: "Context ID (e.g., retailer ID). Defaults to FLUENT_RETAILER_ID for RETAILER context.",
79
+ },
80
+ },
81
+ required: ["name", "value", "context"],
82
+ additionalProperties: false,
83
+ },
84
+ },
85
+ {
86
+ name: "setting.bulkUpsert",
87
+ description: [
88
+ "Batch create or update multiple settings in one call.",
89
+ "",
90
+ "Processes up to 50 settings sequentially with individual error handling.",
91
+ "Returns per-setting results with created/updated/failed counts.",
92
+ ].join("\n"),
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ settings: {
97
+ type: "array",
98
+ minItems: 1,
99
+ maxItems: 50,
100
+ items: {
101
+ type: "object",
102
+ properties: {
103
+ name: { type: "string" },
104
+ value: { type: "string" },
105
+ lobValue: { type: "string" },
106
+ context: { type: "string" },
107
+ contextId: { type: "integer" },
108
+ },
109
+ required: ["name", "value", "context"],
110
+ },
111
+ description: "Array of settings to upsert",
112
+ },
113
+ },
114
+ required: ["settings"],
115
+ additionalProperties: false,
116
+ },
117
+ },
118
+ ];
119
+ function requireSettingClient(ctx) {
120
+ if (!ctx.client) {
121
+ throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run config.validate and fix auth/base URL.");
122
+ }
123
+ return ctx.client;
124
+ }
125
+ function resolveContextId(context, contextId, config) {
126
+ if (contextId !== undefined)
127
+ return contextId;
128
+ if (context === "RETAILER" && config.retailerId) {
129
+ return Number(config.retailerId);
130
+ }
131
+ throw new ToolError("VALIDATION_ERROR", `contextId is required for context "${context}". Set FLUENT_RETAILER_ID or pass contextId explicitly.`);
132
+ }
133
+ /**
134
+ * Query for an existing setting by name, context, and contextId.
135
+ */
136
+ async function findExistingSetting(client, name, context, contextId) {
137
+ const query = `query FindSetting($name: [String!], $context: [String!], $contextId: [Int!]) {
138
+ settings(name: $name, context: $context, contextId: $contextId, first: 1) {
139
+ edges {
140
+ node {
141
+ id
142
+ name
143
+ value
144
+ lobValue
145
+ context
146
+ contextId
147
+ }
148
+ }
149
+ }
150
+ }`;
151
+ const response = await client.graphql({
152
+ query,
153
+ variables: {
154
+ name: [name],
155
+ context: [context],
156
+ contextId: [contextId],
157
+ },
158
+ });
159
+ const data = response?.data;
160
+ const connection = data?.settings;
161
+ const edges = connection?.edges;
162
+ const node = edges?.[0]?.node;
163
+ if (!node)
164
+ return null;
165
+ return {
166
+ id: node.id,
167
+ value: node.value ?? null,
168
+ lobValue: node.lobValue ?? null,
169
+ };
170
+ }
171
+ /**
172
+ * Create a new setting.
173
+ */
174
+ async function createSetting(client, input) {
175
+ const mutationInput = {
176
+ name: input.name,
177
+ context: input.context,
178
+ contextId: input.contextId,
179
+ };
180
+ if (input.lobValue) {
181
+ mutationInput.lobValue = input.lobValue;
182
+ mutationInput.valueType = "LOB";
183
+ }
184
+ else {
185
+ mutationInput.value = input.value;
186
+ mutationInput.valueType = "STRING";
187
+ }
188
+ const mutation = `mutation CreateSetting($input: CreateSettingInput!) {
189
+ createSetting(input: $input) {
190
+ id
191
+ name
192
+ value
193
+ lobValue
194
+ context
195
+ contextId
196
+ }
197
+ }`;
198
+ const response = await client.graphql({
199
+ query: mutation,
200
+ variables: { input: mutationInput },
201
+ }, { retry: false });
202
+ const data = response?.data;
203
+ const setting = data?.createSetting;
204
+ if (!setting) {
205
+ const errors = response?.errors;
206
+ throw new ToolError("SDK_ERROR", "createSetting mutation returned no data", {
207
+ details: { graphqlErrors: errors },
208
+ });
209
+ }
210
+ return setting;
211
+ }
212
+ /**
213
+ * Update an existing setting.
214
+ */
215
+ async function updateSetting(client, input) {
216
+ const mutationInput = {
217
+ id: input.id,
218
+ context: input.context,
219
+ contextId: input.contextId,
220
+ };
221
+ if (input.lobValue) {
222
+ mutationInput.lobValue = input.lobValue;
223
+ mutationInput.valueType = "LOB";
224
+ }
225
+ else {
226
+ mutationInput.value = input.value;
227
+ mutationInput.valueType = "STRING";
228
+ }
229
+ const mutation = `mutation UpdateSetting($input: UpdateSettingInput!) {
230
+ updateSetting(input: $input) {
231
+ id
232
+ name
233
+ value
234
+ lobValue
235
+ context
236
+ contextId
237
+ }
238
+ }`;
239
+ const response = await client.graphql({
240
+ query: mutation,
241
+ variables: { input: mutationInput },
242
+ }, { retry: false });
243
+ const data = response?.data;
244
+ const setting = data?.updateSetting;
245
+ if (!setting) {
246
+ const errors = response?.errors;
247
+ throw new ToolError("SDK_ERROR", "updateSetting mutation returned no data", {
248
+ details: { graphqlErrors: errors },
249
+ });
250
+ }
251
+ return setting;
252
+ }
253
+ /**
254
+ * Handle setting.upsert tool call.
255
+ */
256
+ export async function handleSettingUpsert(args, ctx) {
257
+ const parsed = SettingUpsertInputSchema.parse(args);
258
+ const client = requireSettingClient(ctx);
259
+ const contextId = resolveContextId(parsed.context, parsed.contextId, ctx.config);
260
+ // Check if setting already exists
261
+ const existing = await findExistingSetting(client, parsed.name, parsed.context, contextId);
262
+ if (existing) {
263
+ // Update
264
+ const setting = await updateSetting(client, {
265
+ id: existing.id,
266
+ value: parsed.value,
267
+ lobValue: parsed.lobValue,
268
+ context: parsed.context,
269
+ contextId,
270
+ });
271
+ return {
272
+ ok: true,
273
+ created: false,
274
+ updated: true,
275
+ setting,
276
+ previousValue: existing.value ?? existing.lobValue,
277
+ };
278
+ }
279
+ // Create
280
+ const setting = await createSetting(client, {
281
+ name: parsed.name,
282
+ value: parsed.value,
283
+ lobValue: parsed.lobValue,
284
+ context: parsed.context,
285
+ contextId,
286
+ });
287
+ return {
288
+ ok: true,
289
+ created: true,
290
+ updated: false,
291
+ setting,
292
+ };
293
+ }
294
+ /**
295
+ * Handle setting.bulkUpsert tool call.
296
+ */
297
+ export async function handleSettingBulkUpsert(args, ctx) {
298
+ const parsed = SettingBulkUpsertInputSchema.parse(args);
299
+ const client = requireSettingClient(ctx);
300
+ let created = 0;
301
+ let updated = 0;
302
+ let failed = 0;
303
+ const results = [];
304
+ for (const setting of parsed.settings) {
305
+ try {
306
+ const contextId = resolveContextId(setting.context, setting.contextId, ctx.config);
307
+ const existing = await findExistingSetting(client, setting.name, setting.context, contextId);
308
+ if (existing) {
309
+ await updateSetting(client, {
310
+ id: existing.id,
311
+ value: setting.value,
312
+ lobValue: setting.lobValue,
313
+ context: setting.context,
314
+ contextId,
315
+ });
316
+ updated++;
317
+ results.push({ name: setting.name, status: "updated" });
318
+ }
319
+ else {
320
+ await createSetting(client, {
321
+ name: setting.name,
322
+ value: setting.value,
323
+ lobValue: setting.lobValue,
324
+ context: setting.context,
325
+ contextId,
326
+ });
327
+ created++;
328
+ results.push({ name: setting.name, status: "created" });
329
+ }
330
+ }
331
+ catch (error) {
332
+ failed++;
333
+ results.push({
334
+ name: setting.name,
335
+ status: "failed",
336
+ error: error instanceof Error ? error.message : String(error),
337
+ });
338
+ }
339
+ }
340
+ return {
341
+ ok: true,
342
+ created,
343
+ updated,
344
+ failed,
345
+ total: parsed.settings.length,
346
+ results,
347
+ };
348
+ }