@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.
@@ -2,12 +2,12 @@
2
2
  * GraphQL Query Execution Tools
3
3
  *
4
4
  * Four query/mutation execution tools:
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
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 graphql.listRoots/buildQuery/validate/generateFull.
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: "graphql.query",
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: "graphql.queryAll",
73
- description: "Auto-paginated GraphQL query. Follows cursors, merges edges, deduplicates. Use instead of graphql.query when you need all records.",
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: "graphql.batchMutate",
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: "graphql.introspect",
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 config.validate and fix auth/base URL.");
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 graphql.query tool call.
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 graphql.queryAll tool call.
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 { ok: true, summarized: true, ...summary, pagination };
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 { ok: true, response: result, pagination };
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 graphql.batchMutate tool call.
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 graphql.introspect tool call.
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
- * 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
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: "graphql.listRoots",
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: "graphql.planOperation",
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: "graphql.buildQuery",
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: "graphql.validate",
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: "graphql.generateFull",
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 graphql.listRoots to discover available fields.`);
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
- ? "graphql.queryAll"
470
- : "graphql.query",
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 graphql.listRoots to discover available fields.`);
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 graphql.introspect's force param.
551
+ * Exposed for use by graphql_introspect's force param.
552
552
  */
553
553
  export function clearSchemaCache(ctx) {
554
554
  if (!ctx.client)
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Metrics tools: metrics.query, metrics.topEvents, metrics.healthCheck,
3
- * metrics.sloReport, metrics.labelCatalog, metrics.catalog
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: "metrics.query",
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: "metrics.topEvents",
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: "metrics.healthCheck",
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: "metrics.sloReport",
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: "metrics.labelCatalog",
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: "metrics.metricCatalog",
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: "metrics.planQuery",
185
- description: "Plan PromQL for a metric family: validates labels, picks the right histogram variant, and returns a ready-to-run metrics.query payload.",
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: "metrics.snapshot",
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: "metrics.compare",
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: "metrics.mutationAudit",
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
- "graphql.query": [
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
- "graphql.query": {
537
- family: "graphql.query",
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["graphql.query"],
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 metrics.topEvents and the
974
- * metrics.healthCheck Event API fallback path.
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 config.validate and fix auth/base URL.");
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 metrics.query tool call.
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 metrics.healthCheck tool call.
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 event.list({ eventStatus: \"FAILED\", from: \"<recent>\", count: 50 }) to identify failing events");
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 metrics.sloReport tool call.
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 metrics.topEvents with eventStatus=FAILED and investigate top failures via /fluent-trace.");
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 metrics.sloReport over shorter windows (e.g., 15m).");
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 metrics.query to break down latency by event_name/entity_type and isolate slow workflows.");
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 metrics.labelCatalog tool call.
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 metrics.metricCatalog with includeLiveMetrics=true to inspect exported metric names first.");
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 metrics.metricCatalog tool call.
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 metrics.planQuery tool call.
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 metrics.metricCatalog with includeLiveMetrics=true to inspect live metric names before building PromQL.",
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: "metrics.query",
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: "metrics.query",
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: "metrics.query",
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: "metrics.query",
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 metrics.topEvents tool call.
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
- // metrics.snapshot — compound dashboard in one call
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
- // metrics.compare — compare two adjacent time windows
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 metrics.metricCatalog to discover available metrics." },
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;