@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.
- package/dist/conformance/mcp-2025-11-25.test.js +41 -0
- package/dist/connectors/loki.js +24 -15
- package/dist/connectors/loki.test.js +15 -0
- package/dist/connectors/prometheus.d.ts +1 -0
- package/dist/connectors/prometheus.js +75 -3
- package/dist/connectors/prometheus.test.js +81 -0
- package/dist/context.d.ts +11 -2
- package/dist/context.js +10 -2
- package/dist/context.test.js +6 -0
- package/dist/enrich/ip-dataset.d.ts +25 -0
- package/dist/enrich/ip-dataset.js +113 -0
- package/dist/enrich/ip-dataset.test.d.ts +1 -0
- package/dist/enrich/ip-dataset.test.js +85 -0
- package/dist/index.js +94 -9
- package/dist/tools/enrich-ips.d.ts +30 -0
- package/dist/tools/enrich-ips.js +60 -0
- package/dist/tools/enrich-ips.test.d.ts +1 -0
- package/dist/tools/enrich-ips.test.js +38 -0
- package/dist/tools/query-logs-schema.test.d.ts +1 -0
- package/dist/tools/query-logs-schema.test.js +38 -0
- package/dist/tools/query-logs.d.ts +5 -2
- package/dist/tools/query-logs.js +31 -13
- package/dist/tools/query-metrics.d.ts +7 -3
- package/dist/tools/query-metrics.js +33 -12
- package/dist/tools/query-raw-gate.test.d.ts +1 -0
- package/dist/tools/query-raw-gate.test.js +52 -0
- package/dist/tools/registry-names.d.ts +1 -1
- package/dist/tools/registry-names.js +2 -0
- package/dist/tools/topology.js +14 -0
- package/dist/tools/topology.test.js +15 -0
- package/dist/tools/validation.d.ts +17 -0
- package/dist/tools/validation.js +27 -0
- package/dist/tools/validation.test.js +24 -1
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/tools/topology.js
CHANGED
|
@@ -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.
|
package/dist/tools/validation.js
CHANGED
|
@@ -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