@thotischner/observability-mcp 3.1.1 → 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.
@@ -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.1",
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",