@fluentcommerce/fluent-mcp-extn 0.7.0 → 0.7.3
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 +176 -174
- package/dist/batch-tools.js +21 -21
- package/dist/cache-tools.js +5 -5
- package/dist/config.js +1 -1
- package/dist/entity-tools.js +16 -16
- package/dist/environment-tools.js +10 -10
- package/dist/event-tools.js +45 -28
- package/dist/fluent-client.js +20 -0
- package/dist/graphql-query-tools.js +73 -17
- package/dist/graphql-schema-tools.js +14 -14
- package/dist/metrics-tools.js +87 -40
- package/dist/profile-registry.js +2 -2
- package/dist/response-shaper.js +1 -1
- package/dist/settings-tools.js +62 -26
- package/dist/test-tools.js +4 -4
- package/dist/tools.js +153 -148
- package/dist/workflow-tools.js +34 -44
- package/docs/E2E_TESTING.md +59 -59
- package/docs/HANDOVER_GITHUB_COPILOT.md +9 -9
- package/docs/HANDOVER_GITHUB_REPO_MCP_CONFIG.example.json +12 -12
- package/docs/RUNBOOK.md +38 -38
- package/docs/TOOL_REFERENCE.md +296 -296
- package/package.json +1 -1
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* GraphQL Query Execution Tools
|
|
3
3
|
*
|
|
4
4
|
* Four query/mutation execution tools:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* graphql_query — execute a single GraphQL query or mutation
|
|
6
|
+
* graphql_queryAll — execute with SDK auto-pagination
|
|
7
|
+
* graphql_batchMutate — execute multiple mutations in a single aliased request
|
|
8
|
+
* graphql_introspect — inspect the schema via introspection service
|
|
9
9
|
*
|
|
10
|
-
* NOTE: graphql-schema-tools.ts handles
|
|
10
|
+
* NOTE: graphql-schema-tools.ts handles graphql_listRoots/buildQuery/validate/generateFull.
|
|
11
11
|
* This file handles the query execution tools.
|
|
12
12
|
*/
|
|
13
13
|
import { z } from "zod";
|
|
@@ -58,7 +58,7 @@ export const GraphQLIntrospectInputSchema = z.object({
|
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
59
59
|
export const GRAPHQL_QUERY_TOOL_DEFINITIONS = [
|
|
60
60
|
{
|
|
61
|
-
name: "
|
|
61
|
+
name: "graphql_query",
|
|
62
62
|
description: "Execute a GraphQL query or mutation. Relay pagination: first/after with cursors on edges.",
|
|
63
63
|
annotations: {
|
|
64
64
|
title: "GraphQL Query",
|
|
@@ -69,8 +69,8 @@ export const GRAPHQL_QUERY_TOOL_DEFINITIONS = [
|
|
|
69
69
|
},
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
name: "
|
|
73
|
-
description: "Auto-paginated GraphQL query. Follows cursors, merges edges, deduplicates. Use instead of
|
|
72
|
+
name: "graphql_queryAll",
|
|
73
|
+
description: "Auto-paginated GraphQL query. Follows cursors, merges edges, deduplicates. Use instead of graphql_query when you need all records.",
|
|
74
74
|
annotations: {
|
|
75
75
|
title: "GraphQL Query All",
|
|
76
76
|
readOnlyHint: true,
|
|
@@ -80,7 +80,7 @@ export const GRAPHQL_QUERY_TOOL_DEFINITIONS = [
|
|
|
80
80
|
},
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
|
-
name: "
|
|
83
|
+
name: "graphql_batchMutate",
|
|
84
84
|
description: "Execute up to 50 mutations in one aliased request. Provide mutation name, inputs array, and returnFields.",
|
|
85
85
|
annotations: {
|
|
86
86
|
title: "GraphQL Batch Mutate",
|
|
@@ -91,7 +91,7 @@ export const GRAPHQL_QUERY_TOOL_DEFINITIONS = [
|
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
|
-
name: "
|
|
94
|
+
name: "graphql_introspect",
|
|
95
95
|
description: "Inspect GraphQL schema. Modes: type, mutation, listMutations, listQueries. Cached 24h, force=true to refresh.",
|
|
96
96
|
annotations: {
|
|
97
97
|
title: "Introspect Schema",
|
|
@@ -143,15 +143,43 @@ function introspectCacheKey(input) {
|
|
|
143
143
|
}
|
|
144
144
|
function requireClient(ctx) {
|
|
145
145
|
if (!ctx.client) {
|
|
146
|
-
throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run
|
|
146
|
+
throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run config_validate and fix auth/base URL.");
|
|
147
147
|
}
|
|
148
148
|
return ctx.client;
|
|
149
149
|
}
|
|
150
|
+
function analyzePaginationLimits(pagination, limits) {
|
|
151
|
+
if (!pagination)
|
|
152
|
+
return { truncated: false };
|
|
153
|
+
const totalPages = typeof pagination.totalPages === "number" ? pagination.totalPages : null;
|
|
154
|
+
const totalRecords = typeof pagination.totalRecords === "number" ? pagination.totalRecords : null;
|
|
155
|
+
const pagesReached = typeof pagination.pagesFetched === "number"
|
|
156
|
+
? pagination.pagesFetched >= limits.maxPages
|
|
157
|
+
: totalPages !== null
|
|
158
|
+
? totalPages >= limits.maxPages
|
|
159
|
+
: false;
|
|
160
|
+
const recordsReached = typeof pagination.recordsFetched === "number"
|
|
161
|
+
? pagination.recordsFetched >= limits.maxRecords
|
|
162
|
+
: totalRecords !== null
|
|
163
|
+
? totalRecords >= limits.maxRecords
|
|
164
|
+
: false;
|
|
165
|
+
if (!pagesReached && !recordsReached) {
|
|
166
|
+
return { truncated: false };
|
|
167
|
+
}
|
|
168
|
+
const reasons = [];
|
|
169
|
+
if (pagesReached)
|
|
170
|
+
reasons.push(`page cap ${limits.maxPages}`);
|
|
171
|
+
if (recordsReached)
|
|
172
|
+
reasons.push(`record cap ${limits.maxRecords}`);
|
|
173
|
+
return {
|
|
174
|
+
truncated: true,
|
|
175
|
+
warning: `Auto-pagination may be truncated by the ${reasons.join(" and ")}. Narrow the query or raise the limit if you need a complete result.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
150
178
|
// ---------------------------------------------------------------------------
|
|
151
179
|
// Handlers
|
|
152
180
|
// ---------------------------------------------------------------------------
|
|
153
181
|
/**
|
|
154
|
-
* Handle
|
|
182
|
+
* Handle graphql_query tool call.
|
|
155
183
|
*/
|
|
156
184
|
export async function handleGraphQLQuery(args, ctx) {
|
|
157
185
|
const client = requireClient(ctx);
|
|
@@ -171,7 +199,7 @@ export async function handleGraphQLQuery(args, ctx) {
|
|
|
171
199
|
return { ok: true, response: result };
|
|
172
200
|
}
|
|
173
201
|
/**
|
|
174
|
-
* Handle
|
|
202
|
+
* Handle graphql_queryAll tool call.
|
|
175
203
|
*/
|
|
176
204
|
export async function handleGraphQLQueryAll(args, ctx) {
|
|
177
205
|
const client = requireClient(ctx);
|
|
@@ -190,16 +218,44 @@ export async function handleGraphQLQueryAll(args, ctx) {
|
|
|
190
218
|
};
|
|
191
219
|
const result = await client.graphqlPaginated(payload);
|
|
192
220
|
const pagination = result.extensions?.autoPagination ?? null;
|
|
221
|
+
const paginationInfo = pagination && typeof pagination === "object"
|
|
222
|
+
? pagination
|
|
223
|
+
: null;
|
|
224
|
+
const paginationAnalysis = analyzePaginationLimits(paginationInfo, {
|
|
225
|
+
maxPages: parsed.maxPages,
|
|
226
|
+
maxRecords: parsed.maxRecords,
|
|
227
|
+
});
|
|
193
228
|
if (parsed.summarize) {
|
|
194
229
|
const summary = summarizeConnection(result);
|
|
195
230
|
if (summary) {
|
|
196
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
summarized: true,
|
|
234
|
+
...summary,
|
|
235
|
+
pagination,
|
|
236
|
+
...(paginationAnalysis.truncated
|
|
237
|
+
? {
|
|
238
|
+
_truncated: true,
|
|
239
|
+
_truncationWarning: paginationAnalysis.warning,
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
};
|
|
197
243
|
}
|
|
198
244
|
}
|
|
199
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
response: result,
|
|
248
|
+
pagination,
|
|
249
|
+
...(paginationAnalysis.truncated
|
|
250
|
+
? {
|
|
251
|
+
_truncated: true,
|
|
252
|
+
_truncationWarning: paginationAnalysis.warning,
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
255
|
+
};
|
|
200
256
|
}
|
|
201
257
|
/**
|
|
202
|
-
* Handle
|
|
258
|
+
* Handle graphql_batchMutate tool call.
|
|
203
259
|
*/
|
|
204
260
|
export async function handleGraphQLBatchMutate(args, ctx) {
|
|
205
261
|
const client = requireClient(ctx);
|
|
@@ -249,7 +305,7 @@ export async function handleGraphQLBatchMutate(args, ctx) {
|
|
|
249
305
|
};
|
|
250
306
|
}
|
|
251
307
|
/**
|
|
252
|
-
* Handle
|
|
308
|
+
* Handle graphql_introspect tool call.
|
|
253
309
|
*/
|
|
254
310
|
export async function handleGraphQLIntrospect(args, ctx) {
|
|
255
311
|
const client = requireClient(ctx);
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* GraphQL Schema Tools
|
|
3
3
|
*
|
|
4
4
|
* Four schema-aware tools that close the gap with the official MCP server:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* graphql_listRoots — list query & mutation root fields
|
|
6
|
+
* graphql_buildQuery — generate a query/mutation string from structured inputs
|
|
7
|
+
* graphql_validate — validate a GraphQL document against the live schema
|
|
8
|
+
* graphql_generateFull — generate a maximal-selection query for a root field
|
|
9
9
|
*
|
|
10
10
|
* All tools share a single SchemaService instance (cached, with force-refresh).
|
|
11
11
|
*/
|
|
@@ -346,7 +346,7 @@ function getSchemaService(ctx) {
|
|
|
346
346
|
// ---------------------------------------------------------------------------
|
|
347
347
|
export const GRAPHQL_SCHEMA_TOOL_DEFINITIONS = [
|
|
348
348
|
{
|
|
349
|
-
name: "
|
|
349
|
+
name: "graphql_listRoots",
|
|
350
350
|
description: "List all query and mutation root fields from the live schema.",
|
|
351
351
|
annotations: {
|
|
352
352
|
title: "List Schema Roots",
|
|
@@ -357,7 +357,7 @@ export const GRAPHQL_SCHEMA_TOOL_DEFINITIONS = [
|
|
|
357
357
|
},
|
|
358
358
|
},
|
|
359
359
|
{
|
|
360
|
-
name: "
|
|
360
|
+
name: "graphql_planOperation",
|
|
361
361
|
description: "Inspect a query/mutation root field and return required args, nested input shape, variables template, and a ready-to-run document scaffold. Use when the model is unsure which inputs are mandatory.",
|
|
362
362
|
annotations: {
|
|
363
363
|
title: "Plan GraphQL Operation",
|
|
@@ -368,7 +368,7 @@ export const GRAPHQL_SCHEMA_TOOL_DEFINITIONS = [
|
|
|
368
368
|
},
|
|
369
369
|
},
|
|
370
370
|
{
|
|
371
|
-
name: "
|
|
371
|
+
name: "graphql_buildQuery",
|
|
372
372
|
description: "Generate a GraphQL query/mutation from { rootField, fields, args }. Auto-validates against live schema.",
|
|
373
373
|
annotations: {
|
|
374
374
|
title: "Build GraphQL Query",
|
|
@@ -379,7 +379,7 @@ export const GRAPHQL_SCHEMA_TOOL_DEFINITIONS = [
|
|
|
379
379
|
},
|
|
380
380
|
},
|
|
381
381
|
{
|
|
382
|
-
name: "
|
|
382
|
+
name: "graphql_validate",
|
|
383
383
|
description: "Validate a GraphQL document against the live schema. Returns OK or validation errors.",
|
|
384
384
|
annotations: {
|
|
385
385
|
title: "Validate GraphQL",
|
|
@@ -390,7 +390,7 @@ export const GRAPHQL_SCHEMA_TOOL_DEFINITIONS = [
|
|
|
390
390
|
},
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
|
-
name: "
|
|
393
|
+
name: "graphql_generateFull",
|
|
394
394
|
description: "Generate a maximal-field query for a root field with recursive introspection (depth 1-5, default 2).",
|
|
395
395
|
annotations: {
|
|
396
396
|
title: "Generate Full Query",
|
|
@@ -434,7 +434,7 @@ export async function handlePlanOperation(args, ctx) {
|
|
|
434
434
|
}
|
|
435
435
|
const field = rootType.getFields()[parsed.rootField];
|
|
436
436
|
if (!field) {
|
|
437
|
-
throw new ToolError("VALIDATION_ERROR", `Root field '${parsed.rootField}' not found. Use
|
|
437
|
+
throw new ToolError("VALIDATION_ERROR", `Root field '${parsed.rootField}' not found. Use graphql_listRoots to discover available fields.`);
|
|
438
438
|
}
|
|
439
439
|
const argPlans = (field.args ?? []).map((arg) => {
|
|
440
440
|
const described = describeInputField(schema, arg.type, arg.name);
|
|
@@ -466,8 +466,8 @@ export async function handlePlanOperation(args, ctx) {
|
|
|
466
466
|
variablesTemplate,
|
|
467
467
|
selectionTemplate: includeSelectionSet ? selectionLines : [],
|
|
468
468
|
recommendedExecutionTool: parsed.kind === "query" && selectionLines.some((line) => line.startsWith("edges {"))
|
|
469
|
-
? "
|
|
470
|
-
: "
|
|
469
|
+
? "graphql_queryAll"
|
|
470
|
+
: "graphql_query",
|
|
471
471
|
document,
|
|
472
472
|
};
|
|
473
473
|
}
|
|
@@ -538,7 +538,7 @@ export async function handleGenerateFull(args, ctx) {
|
|
|
538
538
|
}
|
|
539
539
|
const field = rootType.getFields()[parsed.rootField];
|
|
540
540
|
if (!field) {
|
|
541
|
-
throw new ToolError("VALIDATION_ERROR", `Root field '${parsed.rootField}' not found. Use
|
|
541
|
+
throw new ToolError("VALIDATION_ERROR", `Root field '${parsed.rootField}' not found. Use graphql_listRoots to discover available fields.`);
|
|
542
542
|
}
|
|
543
543
|
const returnTypeName = unwrapNamed(field.type).name;
|
|
544
544
|
const selection = buildMaxSelection(schema, returnTypeName, parsed.depth);
|
|
@@ -548,7 +548,7 @@ export async function handleGenerateFull(args, ctx) {
|
|
|
548
548
|
}
|
|
549
549
|
/**
|
|
550
550
|
* Force-clear the schema cache for the current client.
|
|
551
|
-
* Exposed for use by
|
|
551
|
+
* Exposed for use by graphql_introspect's force param.
|
|
552
552
|
*/
|
|
553
553
|
export function clearSchemaCache(ctx) {
|
|
554
554
|
if (!ctx.client)
|
package/dist/metrics-tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Metrics tools:
|
|
3
|
-
*
|
|
2
|
+
* Metrics tools: metrics_query, metrics_topEvents, metrics_healthCheck,
|
|
3
|
+
* metrics_sloReport, metrics_labelCatalog, metrics.catalog
|
|
4
4
|
*
|
|
5
5
|
* Prometheus-backed observability plus Event API fallback for
|
|
6
6
|
* health assessment, SLO reporting, label discovery, and metric planning.
|
|
@@ -10,6 +10,7 @@ import { GraphQLIntrospectionService, } from "@fluentcommerce/fc-connect-sdk";
|
|
|
10
10
|
import { ToolError } from "./errors.js";
|
|
11
11
|
import { classifyMutationOperations, listMutationCatalog, } from "./mutation-taxonomy.js";
|
|
12
12
|
import { parseWindowMs, recommendStep } from "./time-window.js";
|
|
13
|
+
const MAX_METRICS_RANGE_POINTS = 5_000;
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Input schemas
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -151,52 +152,52 @@ export const MetricsMutationAuditInputSchema = z.object({
|
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
152
153
|
export const METRICS_TOOL_DEFINITIONS = [
|
|
153
154
|
{
|
|
154
|
-
name: "
|
|
155
|
+
name: "metrics_query",
|
|
155
156
|
description: "Query Prometheus metrics via GraphQL (instant or range). Requires METRICS_VIEW permission.",
|
|
156
157
|
annotations: { title: "Query Metrics", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
157
158
|
},
|
|
158
159
|
{
|
|
159
|
-
name: "
|
|
160
|
+
name: "metrics_topEvents",
|
|
160
161
|
description: "Aggregate events by name+entityType+status in a time window. Returns top-N rankings.",
|
|
161
162
|
annotations: { title: "Top Events", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
162
163
|
},
|
|
163
164
|
{
|
|
164
|
-
name: "
|
|
165
|
+
name: "metrics_healthCheck",
|
|
165
166
|
description: "Single-call health assessment via Prometheus. Checks failure rate, NO_MATCH, PENDING, dominance. Falls back to Event API.",
|
|
166
167
|
annotations: { title: "Health Check", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
167
168
|
},
|
|
168
169
|
{
|
|
169
|
-
name: "
|
|
170
|
+
name: "metrics_sloReport",
|
|
170
171
|
description: "SLO snapshot: event volume, failure/no-match/pending rates, p95 latency. Prometheus with Event API fallback.",
|
|
171
172
|
annotations: { title: "SLO Report", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
172
173
|
},
|
|
173
174
|
{
|
|
174
|
-
name: "
|
|
175
|
+
name: "metrics_labelCatalog",
|
|
175
176
|
description: "Discover labels for a Prometheus metric with live sampling and known Fluent hints.",
|
|
176
177
|
annotations: { title: "Label Catalog", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
177
178
|
},
|
|
178
179
|
{
|
|
179
|
-
name: "
|
|
180
|
+
name: "metrics_metricCatalog",
|
|
180
181
|
description: "List known Fluent metric families with labels, variants, and example PromQL. Can optionally probe live metric names and labels.",
|
|
181
182
|
annotations: { title: "Metric Catalog", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
182
183
|
},
|
|
183
184
|
{
|
|
184
|
-
name: "
|
|
185
|
-
description: "Plan PromQL for a metric family: validates labels, picks the right histogram variant, and returns a ready-to-run
|
|
185
|
+
name: "metrics_planQuery",
|
|
186
|
+
description: "Plan PromQL for a metric family: validates labels, picks the right histogram variant, and returns a ready-to-run metrics_query payload.",
|
|
186
187
|
annotations: { title: "Plan Metric Query", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
187
188
|
},
|
|
188
189
|
{
|
|
189
|
-
name: "
|
|
190
|
+
name: "metrics_snapshot",
|
|
190
191
|
description: "Dashboard snapshot: runs parallel queries to show event volume, failure rates, breakdowns by entity type, avg latency, and p95 in one call. Uses delta pattern for accurate window counts.",
|
|
191
192
|
annotations: { title: "Metrics Snapshot", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
192
193
|
},
|
|
193
194
|
{
|
|
194
|
-
name: "
|
|
195
|
+
name: "metrics_compare",
|
|
195
196
|
description: "Compare a metric across two adjacent time windows (e.g. last 30m vs preceding 30m). Returns current/previous values, absolute delta, and percentage change per group.",
|
|
196
197
|
annotations: { title: "Compare Windows", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
197
198
|
},
|
|
198
199
|
{
|
|
199
|
-
name: "
|
|
200
|
+
name: "metrics_mutationAudit",
|
|
200
201
|
description: "Audit GraphQL create/update mutation traffic by domain and subdomain. Discovers mutations locally, counts each operation over a fixed window, and aggregates results.",
|
|
201
202
|
annotations: { title: "Mutation Audit", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
202
203
|
},
|
|
@@ -204,6 +205,13 @@ export const METRICS_TOOL_DEFINITIONS = [
|
|
|
204
205
|
// ---------------------------------------------------------------------------
|
|
205
206
|
// Math helpers
|
|
206
207
|
// ---------------------------------------------------------------------------
|
|
208
|
+
function parseIsoTimestamp(value, field) {
|
|
209
|
+
const parsed = new Date(value);
|
|
210
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
211
|
+
throw new ToolError("VALIDATION_ERROR", `metrics_query range ${field} must be a valid ISO-8601 timestamp.`);
|
|
212
|
+
}
|
|
213
|
+
return parsed;
|
|
214
|
+
}
|
|
207
215
|
function round1(value) {
|
|
208
216
|
return Math.round(value * 10) / 10;
|
|
209
217
|
}
|
|
@@ -363,7 +371,7 @@ const KNOWN_METRIC_LABELS = {
|
|
|
363
371
|
"env_tag",
|
|
364
372
|
"environment",
|
|
365
373
|
],
|
|
366
|
-
"
|
|
374
|
+
"graphql_query": [
|
|
367
375
|
"account.name",
|
|
368
376
|
"query.type",
|
|
369
377
|
"field.name",
|
|
@@ -533,12 +541,12 @@ const KNOWN_METRIC_FAMILIES = {
|
|
|
533
541
|
],
|
|
534
542
|
examples: [],
|
|
535
543
|
},
|
|
536
|
-
"
|
|
537
|
-
family: "
|
|
544
|
+
"graphql_query": {
|
|
545
|
+
family: "graphql_query",
|
|
538
546
|
category: "graphql_micrometer",
|
|
539
547
|
kind: "timer",
|
|
540
548
|
description: "Micrometer timer for GraphQL field-level execution.",
|
|
541
|
-
labels: KNOWN_METRIC_LABELS["
|
|
549
|
+
labels: KNOWN_METRIC_LABELS["graphql_query"],
|
|
542
550
|
safeGroupBy: ["query.type", "field.name", "account.name"],
|
|
543
551
|
queryable: false,
|
|
544
552
|
notes: [
|
|
@@ -970,8 +978,8 @@ function summarizeKnownMetricFamily(family, includeExamples) {
|
|
|
970
978
|
// parseWindowMs imported from ./time-window.js (single source of truth)
|
|
971
979
|
/**
|
|
972
980
|
* Fetch events from the Event API with pagination, then aggregate by
|
|
973
|
-
* (name|entityType|status). Shared by
|
|
974
|
-
*
|
|
981
|
+
* (name|entityType|status). Shared by metrics_topEvents and the
|
|
982
|
+
* metrics_healthCheck Event API fallback path.
|
|
975
983
|
*/
|
|
976
984
|
async function aggregateEventsFromApi(client, params) {
|
|
977
985
|
const baseParams = {
|
|
@@ -1022,6 +1030,7 @@ async function aggregateEventsFromApi(client, params) {
|
|
|
1022
1030
|
return {
|
|
1023
1031
|
totalEvents: allEvents.length,
|
|
1024
1032
|
totalPages: page - 1,
|
|
1033
|
+
truncated: hasMore,
|
|
1025
1034
|
statusBreakdown: Object.fromEntries(statusCounts),
|
|
1026
1035
|
groups,
|
|
1027
1036
|
uniqueNames: new Set(allEvents.map((e) => e.name)),
|
|
@@ -1049,7 +1058,7 @@ function rankTopEvents(groups, totalEvents, topN) {
|
|
|
1049
1058
|
// ---------------------------------------------------------------------------
|
|
1050
1059
|
function requireClient(ctx) {
|
|
1051
1060
|
if (!ctx.client) {
|
|
1052
|
-
throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run
|
|
1061
|
+
throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run config_validate and fix auth/base URL.");
|
|
1053
1062
|
}
|
|
1054
1063
|
return ctx.client;
|
|
1055
1064
|
}
|
|
@@ -1057,13 +1066,35 @@ function requireClient(ctx) {
|
|
|
1057
1066
|
// Handlers
|
|
1058
1067
|
// ---------------------------------------------------------------------------
|
|
1059
1068
|
/**
|
|
1060
|
-
* Handle
|
|
1069
|
+
* Handle metrics_query tool call.
|
|
1061
1070
|
*/
|
|
1062
1071
|
export async function handleMetricsQuery(args, ctx) {
|
|
1063
1072
|
const parsed = MetricsQueryInputSchema.parse(args);
|
|
1064
1073
|
if (parsed.type === "range" && (!parsed.start || !parsed.end || !parsed.step)) {
|
|
1065
1074
|
throw new ToolError("VALIDATION_ERROR", "Range queries require start, end, and step.");
|
|
1066
1075
|
}
|
|
1076
|
+
if (parsed.type === "range") {
|
|
1077
|
+
const start = parseIsoTimestamp(parsed.start, "start");
|
|
1078
|
+
const end = parseIsoTimestamp(parsed.end, "end");
|
|
1079
|
+
const durationMs = end.getTime() - start.getTime();
|
|
1080
|
+
if (durationMs <= 0) {
|
|
1081
|
+
throw new ToolError("VALIDATION_ERROR", "metrics_query range end must be after start.");
|
|
1082
|
+
}
|
|
1083
|
+
const stepMs = parseWindowMs(parsed.step);
|
|
1084
|
+
const estimatedPoints = Math.ceil(durationMs / stepMs);
|
|
1085
|
+
if (estimatedPoints > MAX_METRICS_RANGE_POINTS) {
|
|
1086
|
+
const hint = recommendStep(durationMs, Math.min(500, MAX_METRICS_RANGE_POINTS));
|
|
1087
|
+
throw new ToolError("VALIDATION_ERROR", `Range query would request about ${estimatedPoints} points, which exceeds the safety limit of ${MAX_METRICS_RANGE_POINTS}. Increase step to at least ${hint.step}.`, {
|
|
1088
|
+
details: {
|
|
1089
|
+
estimatedPoints,
|
|
1090
|
+
maxPoints: MAX_METRICS_RANGE_POINTS,
|
|
1091
|
+
recommendedStep: hint.step,
|
|
1092
|
+
recommendedStepMs: hint.stepMs,
|
|
1093
|
+
recommendedEstimatedPoints: hint.estimatedPoints,
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1067
1098
|
const client = requireClient(ctx);
|
|
1068
1099
|
const response = await client.queryPrometheus(parsed);
|
|
1069
1100
|
const vectors = extractPrometheusVectors(response);
|
|
@@ -1081,7 +1112,7 @@ export async function handleMetricsQuery(args, ctx) {
|
|
|
1081
1112
|
};
|
|
1082
1113
|
}
|
|
1083
1114
|
/**
|
|
1084
|
-
* Handle
|
|
1115
|
+
* Handle metrics_healthCheck tool call.
|
|
1085
1116
|
*/
|
|
1086
1117
|
export async function handleMetricsHealthCheck(args, ctx) {
|
|
1087
1118
|
const parsed = MetricsHealthCheckInputSchema.parse(args);
|
|
@@ -1092,6 +1123,7 @@ export async function handleMetricsHealthCheck(args, ctx) {
|
|
|
1092
1123
|
let statusBreakdown = {};
|
|
1093
1124
|
let totalEvents = 0;
|
|
1094
1125
|
let topEvents = [];
|
|
1126
|
+
let eventApiTruncated = false;
|
|
1095
1127
|
try {
|
|
1096
1128
|
// Query 1: status breakdown
|
|
1097
1129
|
const statusResponse = await client.queryPrometheus({
|
|
@@ -1154,6 +1186,7 @@ export async function handleMetricsHealthCheck(args, ctx) {
|
|
|
1154
1186
|
});
|
|
1155
1187
|
totalEvents = agg.totalEvents;
|
|
1156
1188
|
statusBreakdown = agg.statusBreakdown;
|
|
1189
|
+
eventApiTruncated = agg.truncated;
|
|
1157
1190
|
if (parsed.includeTopEvents) {
|
|
1158
1191
|
topEvents = rankTopEvents(agg.groups, agg.totalEvents, parsed.topN);
|
|
1159
1192
|
}
|
|
@@ -1212,7 +1245,7 @@ export async function handleMetricsHealthCheck(args, ctx) {
|
|
|
1212
1245
|
// Build recommendations
|
|
1213
1246
|
const recommendations = [];
|
|
1214
1247
|
if (findings.some((f) => f.type === "HIGH_FAILURE_RATE")) {
|
|
1215
|
-
recommendations.push("Run
|
|
1248
|
+
recommendations.push("Run event_list({ eventStatus: \"FAILED\", from: \"<recent>\", count: 50 }) to identify failing events");
|
|
1216
1249
|
recommendations.push("Hand off to /fluent-trace for root cause analysis");
|
|
1217
1250
|
}
|
|
1218
1251
|
if (findings.some((f) => f.type === "NO_MATCH_PRESENT")) {
|
|
@@ -1230,6 +1263,10 @@ export async function handleMetricsHealthCheck(args, ctx) {
|
|
|
1230
1263
|
ok: true,
|
|
1231
1264
|
healthy,
|
|
1232
1265
|
source,
|
|
1266
|
+
...(eventApiTruncated ? {
|
|
1267
|
+
_truncated: true,
|
|
1268
|
+
_truncationWarning: "Event API results truncated — counts may be approximate. Narrow the time window for accuracy.",
|
|
1269
|
+
} : {}),
|
|
1233
1270
|
summary: {
|
|
1234
1271
|
window,
|
|
1235
1272
|
totalEvents,
|
|
@@ -1243,7 +1280,7 @@ export async function handleMetricsHealthCheck(args, ctx) {
|
|
|
1243
1280
|
};
|
|
1244
1281
|
}
|
|
1245
1282
|
/**
|
|
1246
|
-
* Handle
|
|
1283
|
+
* Handle metrics_sloReport tool call.
|
|
1247
1284
|
*/
|
|
1248
1285
|
export async function handleMetricsSloReport(args, ctx) {
|
|
1249
1286
|
const parsed = MetricsSloReportInputSchema.parse(args);
|
|
@@ -1268,6 +1305,7 @@ export async function handleMetricsSloReport(args, ctx) {
|
|
|
1268
1305
|
let pendingEvents = 0;
|
|
1269
1306
|
let runtimeP95Seconds = null;
|
|
1270
1307
|
let inflightP95Seconds = null;
|
|
1308
|
+
let eventApiTruncated = false;
|
|
1271
1309
|
try {
|
|
1272
1310
|
const [totalResponse, failedResponse, noMatchResponse, pendingResponse, runtimeP95Response, inflightP95Response,] = await Promise.all([
|
|
1273
1311
|
client.queryPrometheus({
|
|
@@ -1316,6 +1354,7 @@ export async function handleMetricsSloReport(args, ctx) {
|
|
|
1316
1354
|
pendingEvents = agg.statusBreakdown["PENDING"] ?? 0;
|
|
1317
1355
|
runtimeP95Seconds = null;
|
|
1318
1356
|
inflightP95Seconds = null;
|
|
1357
|
+
eventApiTruncated = agg.truncated;
|
|
1319
1358
|
}
|
|
1320
1359
|
let topFailingEvents;
|
|
1321
1360
|
if (parsed.includeTopFailingEvents) {
|
|
@@ -1386,21 +1425,25 @@ export async function handleMetricsSloReport(args, ctx) {
|
|
|
1386
1425
|
}
|
|
1387
1426
|
const recommendations = [];
|
|
1388
1427
|
if (findings.some((f) => f.type === "HIGH_FAILURE_RATE")) {
|
|
1389
|
-
recommendations.push("Run
|
|
1428
|
+
recommendations.push("Run metrics_topEvents with eventStatus=FAILED and investigate top failures via /fluent-trace.");
|
|
1390
1429
|
}
|
|
1391
1430
|
if (findings.some((f) => f.type === "HIGH_NO_MATCH_RATE")) {
|
|
1392
1431
|
recommendations.push("Validate event names against workflow rulesets using /fluent-workflow-analyzer.");
|
|
1393
1432
|
}
|
|
1394
1433
|
if (findings.some((f) => f.type === "HIGH_PENDING_RATE")) {
|
|
1395
|
-
recommendations.push("Check orchestration backlog and re-run
|
|
1434
|
+
recommendations.push("Check orchestration backlog and re-run metrics_sloReport over shorter windows (e.g., 15m).");
|
|
1396
1435
|
}
|
|
1397
1436
|
if (findings.some((f) => f.type === "HIGH_RUNTIME_P95" || f.type === "HIGH_INFLIGHT_P95")) {
|
|
1398
|
-
recommendations.push("Use
|
|
1437
|
+
recommendations.push("Use metrics_query to break down latency by event_name/entity_type and isolate slow workflows.");
|
|
1399
1438
|
}
|
|
1400
1439
|
return {
|
|
1401
1440
|
ok: true,
|
|
1402
1441
|
source,
|
|
1403
1442
|
healthy: findings.length === 0,
|
|
1443
|
+
...(eventApiTruncated ? {
|
|
1444
|
+
_truncated: true,
|
|
1445
|
+
_truncationWarning: "Event API results truncated — counts may be approximate. Narrow the time window or increase maxPages for accuracy.",
|
|
1446
|
+
} : {}),
|
|
1404
1447
|
summary: {
|
|
1405
1448
|
window,
|
|
1406
1449
|
timeWindow: { from, to },
|
|
@@ -1433,7 +1476,7 @@ export async function handleMetricsSloReport(args, ctx) {
|
|
|
1433
1476
|
};
|
|
1434
1477
|
}
|
|
1435
1478
|
/**
|
|
1436
|
-
* Handle
|
|
1479
|
+
* Handle metrics_labelCatalog tool call.
|
|
1437
1480
|
*/
|
|
1438
1481
|
export async function handleMetricsLabelCatalog(args, ctx) {
|
|
1439
1482
|
const parsed = MetricsLabelCatalogInputSchema.parse(args);
|
|
@@ -1449,7 +1492,7 @@ export async function handleMetricsLabelCatalog(args, ctx) {
|
|
|
1449
1492
|
let sampled = null;
|
|
1450
1493
|
const warnings = [];
|
|
1451
1494
|
if (!sampling.queryable) {
|
|
1452
|
-
warnings.push("This documented metric is a Micrometer meter name, not a guaranteed Prometheus family. Use
|
|
1495
|
+
warnings.push("This documented metric is a Micrometer meter name, not a guaranteed Prometheus family. Use metrics_metricCatalog with includeLiveMetrics=true to inspect exported metric names first.");
|
|
1453
1496
|
}
|
|
1454
1497
|
else {
|
|
1455
1498
|
sampled = await sampleMetricLabels(client, sampling.metrics, parsed.window, parsed.maxValuesPerLabel, knownLabels);
|
|
@@ -1478,7 +1521,7 @@ export async function handleMetricsLabelCatalog(args, ctx) {
|
|
|
1478
1521
|
};
|
|
1479
1522
|
}
|
|
1480
1523
|
/**
|
|
1481
|
-
* Handle
|
|
1524
|
+
* Handle metrics_metricCatalog tool call.
|
|
1482
1525
|
*/
|
|
1483
1526
|
export async function handleMetricsMetricCatalog(args, ctx) {
|
|
1484
1527
|
const parsed = MetricsMetricCatalogInputSchema.parse(args);
|
|
@@ -1572,7 +1615,7 @@ export async function handleMetricsMetricCatalog(args, ctx) {
|
|
|
1572
1615
|
};
|
|
1573
1616
|
}
|
|
1574
1617
|
/**
|
|
1575
|
-
* Handle
|
|
1618
|
+
* Handle metrics_planQuery tool call.
|
|
1576
1619
|
*/
|
|
1577
1620
|
export async function handleMetricsPlanQuery(args) {
|
|
1578
1621
|
const parsed = MetricsPlanQueryInputSchema.parse(args);
|
|
@@ -1585,7 +1628,7 @@ export async function handleMetricsPlanQuery(args) {
|
|
|
1585
1628
|
metric: parsed.metric,
|
|
1586
1629
|
warnings: [
|
|
1587
1630
|
"Metric is not in the built-in catalog.",
|
|
1588
|
-
"Use
|
|
1631
|
+
"Use metrics_metricCatalog with includeLiveMetrics=true to inspect live metric names before building PromQL.",
|
|
1589
1632
|
],
|
|
1590
1633
|
};
|
|
1591
1634
|
}
|
|
@@ -1761,7 +1804,7 @@ export async function handleMetricsPlanQuery(args) {
|
|
|
1761
1804
|
query,
|
|
1762
1805
|
recommendedQueryType: parsed.goal === "delta" || parsed.goal === "recency" || parsed.goal === "raw" ? "instant" : "instant",
|
|
1763
1806
|
executeWith: {
|
|
1764
|
-
tool: "
|
|
1807
|
+
tool: "metrics_query",
|
|
1765
1808
|
arguments: {
|
|
1766
1809
|
query,
|
|
1767
1810
|
type: "instant",
|
|
@@ -1772,7 +1815,7 @@ export async function handleMetricsPlanQuery(args) {
|
|
|
1772
1815
|
recommendedStep: recommendStep(parseWindowMs(parsed.window)).step,
|
|
1773
1816
|
estimatedPoints: recommendStep(parseWindowMs(parsed.window)).estimatedPoints,
|
|
1774
1817
|
executeWith: {
|
|
1775
|
-
tool: "
|
|
1818
|
+
tool: "metrics_query",
|
|
1776
1819
|
arguments: {
|
|
1777
1820
|
query,
|
|
1778
1821
|
type: "range",
|
|
@@ -1785,14 +1828,14 @@ export async function handleMetricsPlanQuery(args) {
|
|
|
1785
1828
|
countAudit: {
|
|
1786
1829
|
...countAudit,
|
|
1787
1830
|
resetAuditExecuteWith: {
|
|
1788
|
-
tool: "
|
|
1831
|
+
tool: "metrics_query",
|
|
1789
1832
|
arguments: {
|
|
1790
1833
|
query: countAudit.resetAuditQuery,
|
|
1791
1834
|
type: "instant",
|
|
1792
1835
|
},
|
|
1793
1836
|
},
|
|
1794
1837
|
comparisonExecuteWith: {
|
|
1795
|
-
tool: "
|
|
1838
|
+
tool: "metrics_query",
|
|
1796
1839
|
arguments: {
|
|
1797
1840
|
query: countAudit.comparisonQuery,
|
|
1798
1841
|
type: "instant",
|
|
@@ -1819,7 +1862,7 @@ export async function handleMetricsPlanQuery(args) {
|
|
|
1819
1862
|
};
|
|
1820
1863
|
}
|
|
1821
1864
|
/**
|
|
1822
|
-
* Handle
|
|
1865
|
+
* Handle metrics_topEvents tool call.
|
|
1823
1866
|
*/
|
|
1824
1867
|
export async function handleMetricsTopEvents(args, ctx) {
|
|
1825
1868
|
const parsed = MetricsTopEventsInputSchema.parse(args);
|
|
@@ -1837,6 +1880,10 @@ export async function handleMetricsTopEvents(args, ctx) {
|
|
|
1837
1880
|
const failedCount = agg.statusBreakdown["FAILED"] ?? 0;
|
|
1838
1881
|
return {
|
|
1839
1882
|
ok: true,
|
|
1883
|
+
...(agg.truncated ? {
|
|
1884
|
+
_truncated: true,
|
|
1885
|
+
_truncationWarning: `Event API results truncated at ${agg.totalPages} pages (${agg.totalEvents} events fetched). Counts may be approximate. Narrow the time window or increase maxPages for accuracy.`,
|
|
1886
|
+
} : {}),
|
|
1840
1887
|
analytics: {
|
|
1841
1888
|
timeWindow: { from: parsed.from, to: toTime },
|
|
1842
1889
|
totalEvents: agg.totalEvents,
|
|
@@ -1852,7 +1899,7 @@ export async function handleMetricsTopEvents(args, ctx) {
|
|
|
1852
1899
|
};
|
|
1853
1900
|
}
|
|
1854
1901
|
// ---------------------------------------------------------------------------
|
|
1855
|
-
//
|
|
1902
|
+
// metrics_snapshot — compound dashboard in one call
|
|
1856
1903
|
// ---------------------------------------------------------------------------
|
|
1857
1904
|
export async function handleMetricsSnapshot(args, ctx) {
|
|
1858
1905
|
const parsed = MetricsSnapshotInputSchema.parse(args);
|
|
@@ -1981,7 +2028,7 @@ export async function handleMetricsSnapshot(args, ctx) {
|
|
|
1981
2028
|
};
|
|
1982
2029
|
}
|
|
1983
2030
|
// ---------------------------------------------------------------------------
|
|
1984
|
-
//
|
|
2031
|
+
// metrics_compare — compare two adjacent time windows
|
|
1985
2032
|
// ---------------------------------------------------------------------------
|
|
1986
2033
|
export async function handleMetricsCompare(args, ctx) {
|
|
1987
2034
|
const parsed = MetricsCompareInputSchema.parse(args);
|
|
@@ -1993,7 +2040,7 @@ export async function handleMetricsCompare(args, ctx) {
|
|
|
1993
2040
|
ok: true,
|
|
1994
2041
|
valid: false,
|
|
1995
2042
|
metric: parsed.metric,
|
|
1996
|
-
error: { code: "UNKNOWN_METRIC", message: "Metric not in catalog or not queryable. Use
|
|
2043
|
+
error: { code: "UNKNOWN_METRIC", message: "Metric not in catalog or not queryable. Use metrics_metricCatalog to discover available metrics." },
|
|
1997
2044
|
};
|
|
1998
2045
|
}
|
|
1999
2046
|
const family = resolved.family;
|