@thotischner/observability-mcp 3.1.0 → 3.2.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.
Files changed (35) hide show
  1. package/dist/conformance/mcp-2025-11-25.test.js +41 -0
  2. package/dist/connectors/loki.js +24 -15
  3. package/dist/connectors/loki.test.js +15 -0
  4. package/dist/connectors/prometheus.d.ts +1 -0
  5. package/dist/connectors/prometheus.js +75 -3
  6. package/dist/connectors/prometheus.test.js +81 -0
  7. package/dist/context.d.ts +11 -2
  8. package/dist/context.js +10 -2
  9. package/dist/context.test.js +6 -0
  10. package/dist/enrich/ip-dataset.d.ts +25 -0
  11. package/dist/enrich/ip-dataset.js +113 -0
  12. package/dist/enrich/ip-dataset.test.d.ts +1 -0
  13. package/dist/enrich/ip-dataset.test.js +85 -0
  14. package/dist/index.js +94 -9
  15. package/dist/tools/enrich-ips.d.ts +30 -0
  16. package/dist/tools/enrich-ips.js +60 -0
  17. package/dist/tools/enrich-ips.test.d.ts +1 -0
  18. package/dist/tools/enrich-ips.test.js +38 -0
  19. package/dist/tools/query-logs-schema.test.d.ts +1 -0
  20. package/dist/tools/query-logs-schema.test.js +38 -0
  21. package/dist/tools/query-logs.d.ts +5 -2
  22. package/dist/tools/query-logs.js +31 -13
  23. package/dist/tools/query-metrics.d.ts +7 -3
  24. package/dist/tools/query-metrics.js +33 -12
  25. package/dist/tools/query-raw-gate.test.d.ts +1 -0
  26. package/dist/tools/query-raw-gate.test.js +52 -0
  27. package/dist/tools/registry-names.d.ts +1 -1
  28. package/dist/tools/registry-names.js +2 -0
  29. package/dist/tools/topology.js +14 -0
  30. package/dist/tools/topology.test.js +15 -0
  31. package/dist/tools/validation.d.ts +17 -0
  32. package/dist/tools/validation.js +27 -0
  33. package/dist/tools/validation.test.js +24 -1
  34. package/dist/types.d.ts +10 -0
  35. package/package.json +1 -1
@@ -146,6 +146,20 @@ export async function getTopologyHandler(registry, args = {}, ctx = defaultConte
146
146
  total: { resources: agg.resources.length, edges: agg.edges.length },
147
147
  truncated,
148
148
  };
149
+ // Signal vs. silence: when NO topology-capable connector contributed a
150
+ // snapshot, an empty {resources:[],edges:[]} is ambiguous to an agent —
151
+ // it can't tell "graph is genuinely empty" from "no topology backend is
152
+ // wired up". Mirror query_traces' explicit "no backend" message so the
153
+ // agent gets a clear signal instead of silence (issue #415).
154
+ if (agg.sources.length === 0) {
155
+ payload.note =
156
+ "No topology-capable connector is configured, so the graph is empty. " +
157
+ "Topology comes from connectors like the built-in `kubernetes` source " +
158
+ "or the aws/gcp/istio/linkerd/consul providers — add one (see the " +
159
+ "Sources tab or docs/plugin-architecture) to populate this graph. " +
160
+ "A deployment with only metrics/logs backends (e.g. Prometheus/Loki) " +
161
+ "has no topology to report here.";
162
+ }
149
163
  return {
150
164
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
151
165
  };
@@ -153,6 +153,21 @@ describe("get_topology tool", () => {
153
153
  assert.equal(out.truncated, true);
154
154
  assert.equal(out.total.resources, fixture().resources.length);
155
155
  });
156
+ it("does not attach the no-connector note when topology is present", async () => {
157
+ const reg = await makeRegistry();
158
+ const out = parseTool(await getTopologyHandler(reg, {}));
159
+ assert.equal(out.note, undefined);
160
+ });
161
+ it("returns an explicit note when no topology connector is configured (issue #415)", async () => {
162
+ // Empty registry — no topology-capable connector. The agent must get a
163
+ // clear signal, not a silent empty graph.
164
+ const reg = new ConnectorRegistry(new PluginLoader());
165
+ const out = parseTool(await getTopologyHandler(reg, {}));
166
+ assert.deepEqual(out.resources, []);
167
+ assert.deepEqual(out.edges, []);
168
+ assert.equal(out.sources.length, 0);
169
+ assert.match(out.note, /no topology-capable connector/i);
170
+ });
156
171
  });
157
172
  describe("get_blast_radius tool", () => {
158
173
  it("reports shared-host blast radius for a co-located pod", async () => {
@@ -16,6 +16,23 @@ export declare function validateServiceName(service: string): string | null;
16
16
  * can't build a pathological query.
17
17
  */
18
18
  export declare function validateLogLabels(labels: unknown): string | null;
19
+ /**
20
+ * Validate a structured `labels` filter map for query_metrics. The rules are
21
+ * identical to the log-label validator (valid Prometheus label names, bounded
22
+ * map size + value length, fail-closed) — metric labels compile to PromQL
23
+ * label-equality matchers and the values are escaped for PromQL at injection
24
+ * time, exactly as the log path escapes for LogQL.
25
+ */
26
+ export declare const validateMetricLabels: typeof validateLogLabels;
27
+ /**
28
+ * Validate a raw PromQL/LogQL passthrough string. The capability gate (is raw
29
+ * query allowed at all) lives at the handler; this only bounds the shape:
30
+ * non-empty string, length-capped so a crafted query can't build a
31
+ * pathological request. The query is sent verbatim to the backend (that is the
32
+ * point of a passthrough), so there is no syntax check — an invalid query just
33
+ * yields the backend's own parse error.
34
+ */
35
+ export declare function validateRawQuery(raw: unknown): string | null;
19
36
  /**
20
37
  * Validate the query_logs `aggregate` spec. Fail-closed, like the labels
21
38
  * validator. Returns an error string or null.
@@ -73,6 +73,33 @@ export function validateLogLabels(labels) {
73
73
  }
74
74
  return null;
75
75
  }
76
+ /**
77
+ * Validate a structured `labels` filter map for query_metrics. The rules are
78
+ * identical to the log-label validator (valid Prometheus label names, bounded
79
+ * map size + value length, fail-closed) — metric labels compile to PromQL
80
+ * label-equality matchers and the values are escaped for PromQL at injection
81
+ * time, exactly as the log path escapes for LogQL.
82
+ */
83
+ export const validateMetricLabels = validateLogLabels;
84
+ /**
85
+ * Validate a raw PromQL/LogQL passthrough string. The capability gate (is raw
86
+ * query allowed at all) lives at the handler; this only bounds the shape:
87
+ * non-empty string, length-capped so a crafted query can't build a
88
+ * pathological request. The query is sent verbatim to the backend (that is the
89
+ * point of a passthrough), so there is no syntax check — an invalid query just
90
+ * yields the backend's own parse error.
91
+ */
92
+ export function validateRawQuery(raw) {
93
+ if (raw === undefined)
94
+ return null;
95
+ if (typeof raw !== "string")
96
+ return "raw_query must be a string.";
97
+ if (raw.trim().length === 0)
98
+ return "raw_query must not be empty.";
99
+ if (raw.length > 8192)
100
+ return "raw_query too long (max 8192 chars).";
101
+ return null;
102
+ }
76
103
  const AGGREGATE_OPS = new Set(["count_over_time", "sum", "topk"]);
77
104
  /**
78
105
  * Validate the query_logs `aggregate` spec. Fail-closed, like the labels
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { validateDuration, validateServiceName, sanitizeLabelValue, validateLogLabels, validateLogAggregate, errorResponse } from "./validation.js";
3
+ import { validateDuration, validateServiceName, sanitizeLabelValue, validateLogLabels, validateLogAggregate, validateMetricLabels, validateRawQuery, errorResponse } from "./validation.js";
4
4
  describe("validateLogAggregate (Q-LOG2)", () => {
5
5
  it("accepts undefined and valid specs", () => {
6
6
  assert.equal(validateLogAggregate(undefined), null);
@@ -54,6 +54,29 @@ describe("validateLogLabels (Q-LOG1)", () => {
54
54
  assert.ok(validateLogLabels(many));
55
55
  });
56
56
  });
57
+ describe("validateRawQuery (R4, issue #415 #3)", () => {
58
+ it("accepts undefined (not requested) and a normal query", () => {
59
+ assert.equal(validateRawQuery(undefined), null);
60
+ assert.equal(validateRawQuery('sum(rate(http_requests_total[5m]))'), null);
61
+ });
62
+ it("rejects non-strings, empty/whitespace, and over-long", () => {
63
+ assert.ok(validateRawQuery(42));
64
+ assert.ok(validateRawQuery(""));
65
+ assert.ok(validateRawQuery(" "));
66
+ assert.ok(validateRawQuery("x".repeat(8193)));
67
+ });
68
+ });
69
+ describe("validateMetricLabels (R3, issue #415 #4)", () => {
70
+ it("accepts undefined and a valid map (same rules as log labels)", () => {
71
+ assert.equal(validateMetricLabels(undefined), null);
72
+ assert.equal(validateMetricLabels({ status: "500", route: "/checkout" }), null);
73
+ });
74
+ it("rejects invalid label names and bad values, fail-closed", () => {
75
+ assert.ok(validateMetricLabels({ "a-b": "x" }));
76
+ assert.ok(validateMetricLabels({ status: 500 }));
77
+ assert.ok(validateMetricLabels("nope"));
78
+ });
79
+ });
57
80
  describe("validateDuration", () => {
58
81
  it("accepts valid durations", () => {
59
82
  assert.equal(validateDuration("5m"), null);
package/dist/types.d.ts CHANGED
@@ -102,6 +102,11 @@ export interface MetricQuery {
102
102
  duration: string;
103
103
  step?: string;
104
104
  groupBy?: string;
105
+ labels?: Record<string, string>;
106
+ /** Raw PromQL, run verbatim over query_range — bypasses the curated metric
107
+ * catalog/selector. When set, `metric`/`groupBy`/`labels`/`service` are
108
+ * ignored. Gated by the OMCP_RAW_QUERY capability at the tool layer. */
109
+ rawQuery?: string;
105
110
  }
106
111
  export interface LogQuery {
107
112
  service: string;
@@ -115,6 +120,11 @@ export interface LogQuery {
115
120
  * environment, …) become first-class selectors instead of brittle
116
121
  * free-text regex. */
117
122
  labels?: Record<string, string>;
123
+ /** Raw LogQL log-selector query, run verbatim — bypasses the curated
124
+ * stream-selector/pipeline construction. When set, `service`/`labels`/
125
+ * `level`/`query` are ignored. Gated by the OMCP_RAW_QUERY capability at
126
+ * the tool layer. For metric LogQL (count_over_time/…) use `aggregate`. */
127
+ rawQuery?: string;
118
128
  }
119
129
  /** Server-side log aggregation (Q-LOG2). Pushes count/group/topk down to
120
130
  * the backend's metric-query path so an agent gets a *number*, not a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thotischner/observability-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Unified observability gateway for AI agents — one MCP server for Prometheus, Loki, and any backend",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",