@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.
- package/LICENSE +21 -0
- package/README.md +818 -0
- package/dist/config.js +195 -0
- package/dist/entity-registry.js +418 -0
- package/dist/entity-tools.js +414 -0
- package/dist/environment-tools.js +573 -0
- package/dist/errors.js +150 -0
- package/dist/event-payload.js +22 -0
- package/dist/fluent-client.js +229 -0
- package/dist/index.js +47 -0
- package/dist/resilience.js +52 -0
- package/dist/response-shaper.js +361 -0
- package/dist/sdk-client.js +237 -0
- package/dist/settings-tools.js +348 -0
- package/dist/test-tools.js +388 -0
- package/dist/tools.js +3254 -0
- package/dist/workflow-tools.js +752 -0
- package/docs/CONTRIBUTING.md +100 -0
- package/docs/E2E_TESTING.md +739 -0
- package/docs/HANDOVER_COPILOT_SETUP_STEPS.example.yml +35 -0
- package/docs/HANDOVER_ENV.example +29 -0
- package/docs/HANDOVER_GITHUB_COPILOT.md +165 -0
- package/docs/HANDOVER_GITHUB_REPO_MCP_CONFIG.example.json +31 -0
- package/docs/HANDOVER_VSCODE_MCP_JSON.example.json +10 -0
- package/docs/IMPLEMENTATION_GUIDE.md +299 -0
- package/docs/RUNBOOK.md +312 -0
- package/docs/TOOL_REFERENCE.md +1810 -0
- package/package.json +68 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity lifecycle tools: entity.create, entity.update, entity.get
|
|
3
|
+
*
|
|
4
|
+
* Type-safe entity CRUD with built-in validation, gotcha knowledge,
|
|
5
|
+
* and audit trail for agentic workflows.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { ToolError } from "./errors.js";
|
|
9
|
+
import { getEntityMeta, buildCreateMutation, buildUpdateMutation, buildGetByIdQuery, buildSearchByRefQuery, validateCreateInput, SUPPORTED_ENTITY_TYPES, } from "./entity-registry.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Input schemas
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export const EntityCreateInputSchema = z.object({
|
|
14
|
+
entityType: z.string().min(1).describe(`Entity type. Supported: ${SUPPORTED_ENTITY_TYPES.join(", ")}`),
|
|
15
|
+
data: z
|
|
16
|
+
.record(z.string(), z.unknown())
|
|
17
|
+
.describe("Entity creation input fields (matches GraphQL create input type)"),
|
|
18
|
+
returnFields: z
|
|
19
|
+
.array(z.string())
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Fields to return after creation. Defaults to entity type defaults."),
|
|
22
|
+
dryRun: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.default(false)
|
|
25
|
+
.describe("If true, build mutation without executing. Returns the GraphQL query."),
|
|
26
|
+
});
|
|
27
|
+
export const EntityUpdateInputSchema = z.object({
|
|
28
|
+
entityType: z.string().min(1),
|
|
29
|
+
id: z.string().min(1).describe("Entity ID (required for updates)"),
|
|
30
|
+
fields: z
|
|
31
|
+
.record(z.string(), z.unknown())
|
|
32
|
+
.describe("Fields to update (matches GraphQL update input type)"),
|
|
33
|
+
returnFields: z
|
|
34
|
+
.array(z.string())
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Fields to return after update."),
|
|
37
|
+
validateTransition: z
|
|
38
|
+
.boolean()
|
|
39
|
+
.default(false)
|
|
40
|
+
.describe("If true and status is being changed, validates the transition is allowed before executing."),
|
|
41
|
+
});
|
|
42
|
+
export const EntityGetInputSchema = z.object({
|
|
43
|
+
entityType: z.string().min(1),
|
|
44
|
+
id: z.string().optional().describe("Entity ID (preferred lookup method)"),
|
|
45
|
+
ref: z.string().optional().describe("Entity ref (fallback when ID unavailable)"),
|
|
46
|
+
fields: z
|
|
47
|
+
.array(z.string())
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Fields to return. Defaults to entity type defaults."),
|
|
50
|
+
includeEdges: z
|
|
51
|
+
.array(z.string())
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Related entity edges to include (e.g., fulfilments, items)"),
|
|
54
|
+
});
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Tool definitions (JSON Schema for MCP)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
export const ENTITY_TOOL_DEFINITIONS = [
|
|
59
|
+
{
|
|
60
|
+
name: "entity.create",
|
|
61
|
+
description: [
|
|
62
|
+
"Type-safe entity creation with built-in validation and gotcha knowledge.",
|
|
63
|
+
"",
|
|
64
|
+
`Supported entity types: ${SUPPORTED_ENTITY_TYPES.join(", ")}`,
|
|
65
|
+
"",
|
|
66
|
+
"KEY FEATURES:",
|
|
67
|
+
"- Validates required fields BEFORE sending (e.g., Location needs openingSchedule)",
|
|
68
|
+
"- Encodes compound key rules (ProductKey needs ref + catalogue.ref)",
|
|
69
|
+
"- Auto-resolves retailerId from config",
|
|
70
|
+
"- dryRun mode returns the mutation without executing",
|
|
71
|
+
"- Returns the executed mutation string for audit trail",
|
|
72
|
+
"",
|
|
73
|
+
"GOTCHAS encoded:",
|
|
74
|
+
"- No create inputs have status field (auto-set to CREATED)",
|
|
75
|
+
"- Customer has NO ref field, username is identifier",
|
|
76
|
+
"- Network uses name not ref in create, retailers is plural array",
|
|
77
|
+
"- Location requires openingSchedule (even for 24/7, use allHours: true)",
|
|
78
|
+
"- Product gtin has 20-char max",
|
|
79
|
+
"- Settings context is plain String, contextId is separate Int",
|
|
80
|
+
].join("\n"),
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
entityType: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: `Entity type. Supported: ${SUPPORTED_ENTITY_TYPES.join(", ")}`,
|
|
87
|
+
},
|
|
88
|
+
data: {
|
|
89
|
+
type: "object",
|
|
90
|
+
additionalProperties: true,
|
|
91
|
+
description: "Entity creation input fields. The structure depends on the entity type.",
|
|
92
|
+
},
|
|
93
|
+
returnFields: {
|
|
94
|
+
type: "array",
|
|
95
|
+
items: { type: "string" },
|
|
96
|
+
description: "Fields to return after creation.",
|
|
97
|
+
},
|
|
98
|
+
dryRun: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
description: "If true, build and validate mutation without executing.",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["entityType", "data"],
|
|
104
|
+
additionalProperties: false,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "entity.update",
|
|
109
|
+
description: [
|
|
110
|
+
"Status-aware entity updates with optional transition validation.",
|
|
111
|
+
"",
|
|
112
|
+
"KEY FEATURES:",
|
|
113
|
+
"- Auto-detects mutation name from entity type",
|
|
114
|
+
"- Optional validateTransition: checks workflow.transitions before status change",
|
|
115
|
+
"- Returns previous status for audit trail",
|
|
116
|
+
"- Supports all entity types in the registry",
|
|
117
|
+
].join("\n"),
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
entityType: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Entity type (ORDER, FULFILMENT, LOCATION, etc.)",
|
|
124
|
+
},
|
|
125
|
+
id: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "Entity ID (required for updates)",
|
|
128
|
+
},
|
|
129
|
+
fields: {
|
|
130
|
+
type: "object",
|
|
131
|
+
additionalProperties: true,
|
|
132
|
+
description: "Fields to update",
|
|
133
|
+
},
|
|
134
|
+
returnFields: {
|
|
135
|
+
type: "array",
|
|
136
|
+
items: { type: "string" },
|
|
137
|
+
description: "Fields to return after update.",
|
|
138
|
+
},
|
|
139
|
+
validateTransition: {
|
|
140
|
+
type: "boolean",
|
|
141
|
+
description: "If true and status is being changed, validates the transition is allowed.",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
required: ["entityType", "id", "fields"],
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "entity.get",
|
|
150
|
+
description: [
|
|
151
|
+
"Unified entity lookup by ID or ref with optional edge inclusion.",
|
|
152
|
+
"",
|
|
153
|
+
"Supports all entity types. Resolves by id (preferred) or ref.",
|
|
154
|
+
"includeEdges fetches related entities (e.g., order.fulfilments).",
|
|
155
|
+
"Default field set per entity type (id, ref, status, type, createdOn, etc.).",
|
|
156
|
+
].join("\n"),
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
entityType: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Entity type (ORDER, FULFILMENT, LOCATION, etc.)",
|
|
163
|
+
},
|
|
164
|
+
id: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Entity ID (preferred lookup method)",
|
|
167
|
+
},
|
|
168
|
+
ref: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Entity ref (fallback when ID unavailable; not available for CUSTOMER)",
|
|
171
|
+
},
|
|
172
|
+
fields: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: { type: "string" },
|
|
175
|
+
description: "Fields to return.",
|
|
176
|
+
},
|
|
177
|
+
includeEdges: {
|
|
178
|
+
type: "array",
|
|
179
|
+
items: { type: "string" },
|
|
180
|
+
description: "Related entity edges to include (e.g., fulfilments, items, attributes)",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
required: ["entityType"],
|
|
184
|
+
additionalProperties: false,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
function requireEntityClient(ctx) {
|
|
189
|
+
if (!ctx.client) {
|
|
190
|
+
throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run config.validate and fix auth/base URL.");
|
|
191
|
+
}
|
|
192
|
+
return ctx.client;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Handle entity.create tool call.
|
|
196
|
+
*/
|
|
197
|
+
export async function handleEntityCreate(args, ctx) {
|
|
198
|
+
const parsed = EntityCreateInputSchema.parse(args);
|
|
199
|
+
const meta = getEntityMeta(parsed.entityType);
|
|
200
|
+
if (!meta) {
|
|
201
|
+
throw new ToolError("VALIDATION_ERROR", `Unsupported entity type: "${parsed.entityType}". Supported: ${SUPPORTED_ENTITY_TYPES.join(", ")}`);
|
|
202
|
+
}
|
|
203
|
+
// Auto-inject retailerId for retailer-scoped entities before validation
|
|
204
|
+
const data = parsed.data;
|
|
205
|
+
if (meta.retailerScoped &&
|
|
206
|
+
!data.retailer &&
|
|
207
|
+
ctx.config.retailerId) {
|
|
208
|
+
data.retailer = { id: Number(ctx.config.retailerId) };
|
|
209
|
+
}
|
|
210
|
+
// Validate required fields (after auto-injection)
|
|
211
|
+
const stillMissing = validateCreateInput(parsed.entityType, data);
|
|
212
|
+
const built = buildCreateMutation(parsed.entityType, parsed.returnFields);
|
|
213
|
+
if (!built) {
|
|
214
|
+
throw new ToolError("VALIDATION_ERROR", `Cannot build create mutation for entity type: "${parsed.entityType}"`);
|
|
215
|
+
}
|
|
216
|
+
if (stillMissing.length > 0) {
|
|
217
|
+
throw new ToolError("VALIDATION_ERROR", `Missing required fields for ${parsed.entityType}: ${stillMissing.join(", ")}`, {
|
|
218
|
+
details: {
|
|
219
|
+
missingFields: stillMissing,
|
|
220
|
+
requiredFields: meta.requiredCreateFields,
|
|
221
|
+
gotchas: meta.gotchas,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (parsed.dryRun) {
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
dryRun: true,
|
|
229
|
+
entityType: parsed.entityType,
|
|
230
|
+
mutation: built.mutation,
|
|
231
|
+
inputType: built.inputType,
|
|
232
|
+
variables: { input: data },
|
|
233
|
+
requiredFields: meta.requiredCreateFields,
|
|
234
|
+
gotchas: meta.gotchas,
|
|
235
|
+
note: "No API call made. Set dryRun=false to execute.",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const client = requireEntityClient(ctx);
|
|
239
|
+
const payload = {
|
|
240
|
+
query: built.mutation,
|
|
241
|
+
variables: { input: data },
|
|
242
|
+
};
|
|
243
|
+
// Use executeOnce semantics (no retry for mutations)
|
|
244
|
+
const response = await client.graphql(payload, { retry: false });
|
|
245
|
+
// Extract the created entity from the response
|
|
246
|
+
const gqlData = response?.data;
|
|
247
|
+
const entity = gqlData?.[meta.createMutation];
|
|
248
|
+
if (!entity) {
|
|
249
|
+
const errors = response?.errors;
|
|
250
|
+
throw new ToolError("SDK_ERROR", "Create mutation returned no data", {
|
|
251
|
+
details: { graphqlErrors: errors },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
entityType: parsed.entityType,
|
|
257
|
+
entity,
|
|
258
|
+
mutation: built.mutation,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Handle entity.update tool call.
|
|
263
|
+
*/
|
|
264
|
+
export async function handleEntityUpdate(args, ctx) {
|
|
265
|
+
const parsed = EntityUpdateInputSchema.parse(args);
|
|
266
|
+
const meta = getEntityMeta(parsed.entityType);
|
|
267
|
+
if (!meta) {
|
|
268
|
+
throw new ToolError("VALIDATION_ERROR", `Unsupported entity type: "${parsed.entityType}". Supported: ${SUPPORTED_ENTITY_TYPES.join(", ")}`);
|
|
269
|
+
}
|
|
270
|
+
const client = requireEntityClient(ctx);
|
|
271
|
+
const fields = parsed.fields;
|
|
272
|
+
// If validateTransition is requested and status is changing, check transitions first
|
|
273
|
+
let transitionWarning;
|
|
274
|
+
if (parsed.validateTransition && fields.status) {
|
|
275
|
+
try {
|
|
276
|
+
// Get current entity state to know current status
|
|
277
|
+
const currentQuery = `query($id: ID!) { ${meta.queryById}(id: $id) { id status type } }`;
|
|
278
|
+
const currentResponse = await client.graphql({
|
|
279
|
+
query: currentQuery,
|
|
280
|
+
variables: { id: parsed.id },
|
|
281
|
+
});
|
|
282
|
+
const currentData = currentResponse?.data?.[meta.queryById];
|
|
283
|
+
if (currentData) {
|
|
284
|
+
const trigger = {
|
|
285
|
+
type: parsed.entityType,
|
|
286
|
+
status: currentData.status,
|
|
287
|
+
retailerId: ctx.config.retailerId ?? undefined,
|
|
288
|
+
};
|
|
289
|
+
if (currentData.type) {
|
|
290
|
+
trigger.flexType = `${parsed.entityType}::${currentData.type}`;
|
|
291
|
+
}
|
|
292
|
+
const transitions = await client.getTransitions({
|
|
293
|
+
triggers: [trigger],
|
|
294
|
+
});
|
|
295
|
+
const transArr = Array.isArray(transitions)
|
|
296
|
+
? transitions
|
|
297
|
+
: [transitions];
|
|
298
|
+
const availableEvents = transArr.flatMap((t) => {
|
|
299
|
+
const rec = t;
|
|
300
|
+
const actions = rec?.userActions ?? rec?.transitions ?? [];
|
|
301
|
+
return Array.isArray(actions) ? actions : [];
|
|
302
|
+
});
|
|
303
|
+
if (availableEvents.length === 0) {
|
|
304
|
+
transitionWarning = `No workflow transitions available from status "${currentData.status}" for ${parsed.entityType}. The status update may succeed as a direct mutation but will not trigger workflow orchestration.`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
transitionWarning = `Could not fetch current entity state for transition validation (entity ID: ${parsed.id}).`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Transition API failure is non-blocking — proceed with update
|
|
313
|
+
transitionWarning = "Transition validation skipped — transitions API unavailable.";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const built = buildUpdateMutation(parsed.entityType, parsed.returnFields);
|
|
317
|
+
if (!built) {
|
|
318
|
+
throw new ToolError("VALIDATION_ERROR", `Cannot build update mutation for entity type: "${parsed.entityType}"`);
|
|
319
|
+
}
|
|
320
|
+
const input = { id: parsed.id, ...fields };
|
|
321
|
+
const payload = {
|
|
322
|
+
query: built.mutation,
|
|
323
|
+
variables: { input },
|
|
324
|
+
};
|
|
325
|
+
const response = await client.graphql(payload, { retry: false });
|
|
326
|
+
const gqlData = response?.data;
|
|
327
|
+
const entity = gqlData?.[meta.updateMutation];
|
|
328
|
+
if (!entity) {
|
|
329
|
+
const errors = response?.errors;
|
|
330
|
+
throw new ToolError("SDK_ERROR", "Update mutation returned no data", {
|
|
331
|
+
details: { graphqlErrors: errors },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
entityType: parsed.entityType,
|
|
337
|
+
entity,
|
|
338
|
+
mutation: built.mutation,
|
|
339
|
+
...(transitionWarning ? { transitionWarning } : {}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Handle entity.get tool call.
|
|
344
|
+
*/
|
|
345
|
+
export async function handleEntityGet(args, ctx) {
|
|
346
|
+
const parsed = EntityGetInputSchema.parse(args);
|
|
347
|
+
const meta = getEntityMeta(parsed.entityType);
|
|
348
|
+
if (!meta) {
|
|
349
|
+
throw new ToolError("VALIDATION_ERROR", `Unsupported entity type: "${parsed.entityType}". Supported: ${SUPPORTED_ENTITY_TYPES.join(", ")}`);
|
|
350
|
+
}
|
|
351
|
+
if (!parsed.id && !parsed.ref) {
|
|
352
|
+
throw new ToolError("VALIDATION_ERROR", "Either id or ref must be provided for entity lookup.");
|
|
353
|
+
}
|
|
354
|
+
const client = requireEntityClient(ctx);
|
|
355
|
+
// Prefer ID lookup
|
|
356
|
+
if (parsed.id) {
|
|
357
|
+
const built = buildGetByIdQuery(parsed.entityType, parsed.includeEdges);
|
|
358
|
+
if (!built) {
|
|
359
|
+
throw new ToolError("VALIDATION_ERROR", `Cannot build ID query for entity type: "${parsed.entityType}"`);
|
|
360
|
+
}
|
|
361
|
+
const response = await client.graphql({
|
|
362
|
+
query: built.query,
|
|
363
|
+
variables: { id: parsed.id },
|
|
364
|
+
});
|
|
365
|
+
const gqlData = response?.data;
|
|
366
|
+
const entity = gqlData?.[built.queryRoot];
|
|
367
|
+
if (!entity) {
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
found: false,
|
|
371
|
+
entityType: parsed.entityType,
|
|
372
|
+
lookupBy: "id",
|
|
373
|
+
lookupValue: parsed.id,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
found: true,
|
|
379
|
+
entityType: parsed.entityType,
|
|
380
|
+
entity,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// Ref-based lookup
|
|
384
|
+
if (!meta.hasRef) {
|
|
385
|
+
throw new ToolError("VALIDATION_ERROR", `Entity type ${parsed.entityType} does not support ref-based lookup. Use id instead.`);
|
|
386
|
+
}
|
|
387
|
+
const built = buildSearchByRefQuery(parsed.entityType, parsed.includeEdges);
|
|
388
|
+
if (!built) {
|
|
389
|
+
throw new ToolError("VALIDATION_ERROR", `Cannot build ref query for entity type: "${parsed.entityType}"`);
|
|
390
|
+
}
|
|
391
|
+
const response = await client.graphql({
|
|
392
|
+
query: built.query,
|
|
393
|
+
variables: { ref: [parsed.ref] },
|
|
394
|
+
});
|
|
395
|
+
const gqlData = response?.data;
|
|
396
|
+
const connection = gqlData?.[built.queryRoot];
|
|
397
|
+
const edges = connection?.edges;
|
|
398
|
+
const entity = edges?.[0]?.node;
|
|
399
|
+
if (!entity) {
|
|
400
|
+
return {
|
|
401
|
+
ok: true,
|
|
402
|
+
found: false,
|
|
403
|
+
entityType: parsed.entityType,
|
|
404
|
+
lookupBy: "ref",
|
|
405
|
+
lookupValue: parsed.ref,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
found: true,
|
|
411
|
+
entityType: parsed.entityType,
|
|
412
|
+
entity,
|
|
413
|
+
};
|
|
414
|
+
}
|