@thotischner/observability-mcp 1.3.4 → 1.4.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/config/loader.test.js +3 -3
- package/dist/connectors/loader.d.ts +43 -0
- package/dist/connectors/loader.js +170 -0
- package/dist/connectors/loki.js +3 -2
- package/dist/connectors/registry.d.ts +3 -0
- package/dist/connectors/registry.js +16 -16
- package/dist/connectors/tls.test.js +3 -3
- package/dist/index.js +91 -7
- package/dist/metrics/instrument-connector.d.ts +8 -0
- package/dist/metrics/instrument-connector.js +41 -0
- package/dist/metrics/self.d.ts +12 -0
- package/dist/metrics/self.js +61 -0
- package/dist/openapi.d.ts +2 -0
- package/dist/openapi.js +186 -0
- package/dist/sdk/index.d.ts +46 -0
- package/dist/sdk/index.js +13 -0
- package/dist/sdk/manifest-schema.d.ts +27 -0
- package/dist/sdk/manifest-schema.js +36 -0
- package/dist/sdk/manifest-schema.test.d.ts +1 -0
- package/dist/sdk/manifest-schema.test.js +50 -0
- package/dist/tools/get-service-health.js +3 -2
- package/dist/ui/index.html +568 -111
- package/dist/util/sanitize.d.ts +1 -0
- package/dist/util/sanitize.js +6 -0
- package/package.json +13 -5
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Hand-written OpenAPI 3.1 spec for the /api/* surface served by
|
|
2
|
+
// mcp-server. The /mcp endpoint follows the MCP Streamable HTTP spec
|
|
3
|
+
// (https://spec.modelcontextprotocol.io/) and is intentionally NOT
|
|
4
|
+
// described here; clients should use the MCP `tools/list` to discover
|
|
5
|
+
// the seven tools exposed there.
|
|
6
|
+
//
|
|
7
|
+
// Keep this lean — operators import it into Insomnia/Postman/OpenAPI
|
|
8
|
+
// codegens. If a path is missing it just won't appear in their UI.
|
|
9
|
+
const SOURCE_SCHEMA = {
|
|
10
|
+
type: "object",
|
|
11
|
+
required: ["name", "type", "url"],
|
|
12
|
+
properties: {
|
|
13
|
+
name: { type: "string", description: "Unique source name." },
|
|
14
|
+
type: { type: "string", description: "Connector type id, e.g. 'prometheus'." },
|
|
15
|
+
url: { type: "string", format: "uri" },
|
|
16
|
+
enabled: { type: "boolean", default: true },
|
|
17
|
+
auth: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
type: { type: "string", enum: ["none", "basic", "bearer"] },
|
|
21
|
+
},
|
|
22
|
+
additionalProperties: true,
|
|
23
|
+
},
|
|
24
|
+
tls: { type: "object", additionalProperties: true },
|
|
25
|
+
signalType: { type: "string", enum: ["metrics", "logs", "traces"] },
|
|
26
|
+
},
|
|
27
|
+
additionalProperties: true,
|
|
28
|
+
};
|
|
29
|
+
export function buildOpenApiSpec(version) {
|
|
30
|
+
// openapi-types' deeply-nested generics make literal path objects fail
|
|
31
|
+
// structural assignability even when the document is valid OpenAPI. We
|
|
32
|
+
// build it as a permissive object and cast at the boundary — the shape
|
|
33
|
+
// is hand-verified and rendered by Swagger/Insomnia downstream.
|
|
34
|
+
const doc = {
|
|
35
|
+
openapi: "3.1.0",
|
|
36
|
+
info: {
|
|
37
|
+
title: "observability-mcp HTTP API",
|
|
38
|
+
version,
|
|
39
|
+
description: "Operator-facing REST API used by the Web UI. The MCP protocol surface lives at /mcp (Streamable HTTP) and is not described here — use MCP's tools/list to discover those.",
|
|
40
|
+
contact: {
|
|
41
|
+
name: "observability-mcp",
|
|
42
|
+
url: "https://github.com/ThoTischner/observability-mcp",
|
|
43
|
+
},
|
|
44
|
+
license: { name: "MIT" },
|
|
45
|
+
},
|
|
46
|
+
servers: [{ url: "/", description: "Current server" }],
|
|
47
|
+
tags: [
|
|
48
|
+
{ name: "sources", description: "Observability backend configuration." },
|
|
49
|
+
{ name: "services", description: "Service discovery across all backends." },
|
|
50
|
+
{ name: "health", description: "Aggregated health for discovered services." },
|
|
51
|
+
{ name: "settings", description: "Runtime server configuration." },
|
|
52
|
+
{ name: "metrics-config", description: "Per-source metric definitions." },
|
|
53
|
+
{ name: "self", description: "Server liveness and Prometheus metrics." },
|
|
54
|
+
],
|
|
55
|
+
paths: {
|
|
56
|
+
"/api/sources": {
|
|
57
|
+
get: {
|
|
58
|
+
tags: ["sources"],
|
|
59
|
+
summary: "List configured sources with live health.",
|
|
60
|
+
responses: {
|
|
61
|
+
"200": {
|
|
62
|
+
description: "Sources with status, latency, signal type.",
|
|
63
|
+
content: { "application/json": { schema: { type: "array", items: SOURCE_SCHEMA } } },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
post: {
|
|
68
|
+
tags: ["sources"],
|
|
69
|
+
summary: "Add a new source.",
|
|
70
|
+
requestBody: { required: true, content: { "application/json": { schema: SOURCE_SCHEMA } } },
|
|
71
|
+
responses: {
|
|
72
|
+
"201": { description: "Source created." },
|
|
73
|
+
"400": { description: "Validation error." },
|
|
74
|
+
"409": { description: "Source with that name already exists." },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
"/api/sources/test": {
|
|
79
|
+
post: {
|
|
80
|
+
tags: ["sources"],
|
|
81
|
+
summary: "Connection check against a source config without persisting it.",
|
|
82
|
+
requestBody: { required: true, content: { "application/json": { schema: SOURCE_SCHEMA } } },
|
|
83
|
+
responses: {
|
|
84
|
+
"200": {
|
|
85
|
+
description: "Reachability report.",
|
|
86
|
+
content: {
|
|
87
|
+
"application/json": {
|
|
88
|
+
schema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
status: { type: "string", enum: ["up", "down"] },
|
|
92
|
+
latencyMs: { type: "number" },
|
|
93
|
+
message: { type: "string", nullable: true },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
"/api/sources/{name}": {
|
|
103
|
+
parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
|
|
104
|
+
put: {
|
|
105
|
+
tags: ["sources"],
|
|
106
|
+
summary: "Replace an existing source.",
|
|
107
|
+
requestBody: { required: true, content: { "application/json": { schema: SOURCE_SCHEMA } } },
|
|
108
|
+
responses: { "200": { description: "Updated." }, "404": { description: "Not found." } },
|
|
109
|
+
},
|
|
110
|
+
delete: {
|
|
111
|
+
tags: ["sources"],
|
|
112
|
+
summary: "Remove a source.",
|
|
113
|
+
responses: { "204": { description: "Removed." }, "404": { description: "Not found." } },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"/api/source-types": {
|
|
117
|
+
get: {
|
|
118
|
+
tags: ["sources"],
|
|
119
|
+
summary: "List connector type ids the server can load (builtin + filesystem plugins).",
|
|
120
|
+
responses: {
|
|
121
|
+
"200": {
|
|
122
|
+
description: "Connector type ids.",
|
|
123
|
+
content: { "application/json": { schema: { type: "array", items: { type: "string" } } } },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
"/api/services": {
|
|
129
|
+
get: {
|
|
130
|
+
tags: ["services"],
|
|
131
|
+
summary: "List services discovered across all connected backends.",
|
|
132
|
+
responses: { "200": { description: "Services." } },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
"/api/health": {
|
|
136
|
+
get: {
|
|
137
|
+
tags: ["health"],
|
|
138
|
+
summary: "Aggregated health map: { [service]: ServiceHealth }.",
|
|
139
|
+
responses: { "200": { description: "Health map." } },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
"/api/health/{service}": {
|
|
143
|
+
parameters: [{ name: "service", in: "path", required: true, schema: { type: "string" } }],
|
|
144
|
+
get: {
|
|
145
|
+
tags: ["health"],
|
|
146
|
+
summary: "Aggregated health for one service.",
|
|
147
|
+
responses: { "200": { description: "ServiceHealth." }, "404": { description: "Not found." } },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
"/api/settings": {
|
|
151
|
+
get: { tags: ["settings"], summary: "Get runtime settings.", responses: { "200": { description: "Settings." } } },
|
|
152
|
+
put: { tags: ["settings"], summary: "Update runtime settings.", responses: { "200": { description: "Updated." } } },
|
|
153
|
+
},
|
|
154
|
+
"/api/health-thresholds": {
|
|
155
|
+
get: { tags: ["settings"], summary: "Get health-score thresholds.", responses: { "200": { description: "Thresholds." } } },
|
|
156
|
+
put: { tags: ["settings"], summary: "Update health-score thresholds.", responses: { "200": { description: "Updated." } } },
|
|
157
|
+
},
|
|
158
|
+
"/api/sources/{name}/metrics": {
|
|
159
|
+
parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
|
|
160
|
+
get: { tags: ["metrics-config"], summary: "Get the active metrics list for this source.", responses: { "200": { description: "Metrics." } } },
|
|
161
|
+
put: { tags: ["metrics-config"], summary: "Replace the active metrics list.", responses: { "200": { description: "Updated." } } },
|
|
162
|
+
delete: { tags: ["metrics-config"], summary: "Reset to connector defaults.", responses: { "200": { description: "Reset." } } },
|
|
163
|
+
},
|
|
164
|
+
"/metrics": {
|
|
165
|
+
get: {
|
|
166
|
+
tags: ["self"],
|
|
167
|
+
summary: "Prometheus scrape endpoint. Toggle with METRICS_ENABLED=false.",
|
|
168
|
+
responses: {
|
|
169
|
+
"200": {
|
|
170
|
+
description: "OpenMetrics text exposition.",
|
|
171
|
+
content: { "text/plain": { schema: { type: "string" } } },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
"/api/openapi.json": {
|
|
177
|
+
get: {
|
|
178
|
+
tags: ["self"],
|
|
179
|
+
summary: "This document.",
|
|
180
|
+
responses: { "200": { description: "OpenAPI 3.1 document." } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
return doc;
|
|
186
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type { ObservabilityConnector } from "../connectors/interface.js";
|
|
2
|
+
export { manifestSchema } from "./manifest-schema.js";
|
|
3
|
+
export type { ValidatedConnectorManifest } from "./manifest-schema.js";
|
|
4
|
+
export type { SignalType, SourceConfig, SourceAuth, SourceTls, ConnectorHealth, ServiceInfo, MetricInfo, MetricQuery, MetricResult, MetricSummary, DataPoint, LogQuery, LogResult, LogEntry, LogSummary, MetricDefinition, } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Manifest shape declared in a plugin's `manifest.json`. The server
|
|
7
|
+
* validates plugin manifests against this at load time.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/plugin-architecture.md
|
|
10
|
+
*/
|
|
11
|
+
export interface ConnectorManifest {
|
|
12
|
+
/** Manifest format version. Always 1 today. */
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
/** Connector type id, e.g. "prometheus". Used in sources.yaml `type:`. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Human-readable name shown in the Web UI / hub. */
|
|
17
|
+
displayName: string;
|
|
18
|
+
/** Semver of this connector build. */
|
|
19
|
+
version: string;
|
|
20
|
+
description: string;
|
|
21
|
+
signalTypes: Array<"metrics" | "logs" | "traces">;
|
|
22
|
+
homepage?: string;
|
|
23
|
+
license?: string;
|
|
24
|
+
logo?: string;
|
|
25
|
+
/** JSON Schema describing this connector's `SourceConfig` payload. */
|
|
26
|
+
configSchema?: unknown;
|
|
27
|
+
capabilities?: {
|
|
28
|
+
queryMetrics?: boolean;
|
|
29
|
+
queryLogs?: boolean;
|
|
30
|
+
listServices?: boolean;
|
|
31
|
+
listAvailableMetrics?: boolean;
|
|
32
|
+
};
|
|
33
|
+
compat?: {
|
|
34
|
+
/** Semver range of mcp-server versions this connector supports. */
|
|
35
|
+
serverVersion?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The default export shape a connector plugin module must provide.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* import type { ObservabilityConnector, ConnectorFactory } from "@thotischner/observability-mcp-sdk";
|
|
43
|
+
* const create: ConnectorFactory = () => new MyConnector();
|
|
44
|
+
* export default create;
|
|
45
|
+
*/
|
|
46
|
+
export type ConnectorFactory = () => import("../connectors/interface.js").ObservabilityConnector | Promise<import("../connectors/interface.js").ObservabilityConnector>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Public plugin SDK — the surface a connector author depends on.
|
|
2
|
+
//
|
|
3
|
+
// Plugins should import only from this path so internal refactors of
|
|
4
|
+
// mcp-server stay invisible to them. This file is intentionally a
|
|
5
|
+
// re-export barrel — keep behaviour out of it.
|
|
6
|
+
//
|
|
7
|
+
// Stability: while still on 0.x of the plugin contract, breaking changes
|
|
8
|
+
// may happen. Each release of mcp-server publishes a sibling
|
|
9
|
+
// `@thotischner/observability-mcp-sdk` package mirroring this surface,
|
|
10
|
+
// pinned to the same version. Plugins should declare a peer/range
|
|
11
|
+
// against that package and an `observabilityMcp.compat.serverVersion`
|
|
12
|
+
// constraint in their package.json (see docs/plugin-architecture.md).
|
|
13
|
+
export { manifestSchema } from "./manifest-schema.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const manifestSchema: z.ZodObject<{
|
|
3
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
displayName: z.ZodString;
|
|
6
|
+
version: z.ZodString;
|
|
7
|
+
description: z.ZodString;
|
|
8
|
+
signalTypes: z.ZodArray<z.ZodEnum<{
|
|
9
|
+
metrics: "metrics";
|
|
10
|
+
logs: "logs";
|
|
11
|
+
traces: "traces";
|
|
12
|
+
}>>;
|
|
13
|
+
homepage: z.ZodOptional<z.ZodString>;
|
|
14
|
+
license: z.ZodOptional<z.ZodString>;
|
|
15
|
+
logo: z.ZodOptional<z.ZodString>;
|
|
16
|
+
configSchema: z.ZodOptional<z.ZodUnknown>;
|
|
17
|
+
capabilities: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
queryMetrics: z.ZodOptional<z.ZodBoolean>;
|
|
19
|
+
queryLogs: z.ZodOptional<z.ZodBoolean>;
|
|
20
|
+
listServices: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
listAvailableMetrics: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
compat: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
serverVersion: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
export type ValidatedConnectorManifest = z.infer<typeof manifestSchema>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Zod schema for connector plugin manifests. Mirror of the TS type in
|
|
3
|
+
// ./index.ts (ConnectorManifest); kept here so both server and plugin
|
|
4
|
+
// authors can import and validate at runtime.
|
|
5
|
+
//
|
|
6
|
+
// Bumping `schemaVersion` is a breaking change for the plugin contract.
|
|
7
|
+
// See docs/plugin-architecture.md for the deprecation policy.
|
|
8
|
+
export const manifestSchema = z.object({
|
|
9
|
+
schemaVersion: z.literal(1),
|
|
10
|
+
name: z.string().min(1).regex(/^[a-z][a-z0-9-]*$/, {
|
|
11
|
+
message: "name must be kebab-case lowercase ASCII",
|
|
12
|
+
}),
|
|
13
|
+
displayName: z.string().min(1),
|
|
14
|
+
version: z.string().regex(/^\d+\.\d+\.\d+(-[a-z0-9.-]+)?$/, {
|
|
15
|
+
message: "version must be semver",
|
|
16
|
+
}),
|
|
17
|
+
description: z.string().min(1),
|
|
18
|
+
signalTypes: z.array(z.enum(["metrics", "logs", "traces"])).min(1),
|
|
19
|
+
homepage: z.string().url().optional(),
|
|
20
|
+
license: z.string().optional(),
|
|
21
|
+
logo: z.string().optional(),
|
|
22
|
+
configSchema: z.unknown().optional(),
|
|
23
|
+
capabilities: z
|
|
24
|
+
.object({
|
|
25
|
+
queryMetrics: z.boolean().optional(),
|
|
26
|
+
queryLogs: z.boolean().optional(),
|
|
27
|
+
listServices: z.boolean().optional(),
|
|
28
|
+
listAvailableMetrics: z.boolean().optional(),
|
|
29
|
+
})
|
|
30
|
+
.optional(),
|
|
31
|
+
compat: z
|
|
32
|
+
.object({
|
|
33
|
+
serverVersion: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
.optional(),
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { manifestSchema } from "./manifest-schema.js";
|
|
4
|
+
const minimal = {
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
name: "prometheus",
|
|
7
|
+
displayName: "Prometheus",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
description: "PromQL backend",
|
|
10
|
+
signalTypes: ["metrics"],
|
|
11
|
+
};
|
|
12
|
+
describe("manifestSchema", () => {
|
|
13
|
+
it("accepts a minimal valid manifest", () => {
|
|
14
|
+
const r = manifestSchema.safeParse(minimal);
|
|
15
|
+
assert.equal(r.success, true);
|
|
16
|
+
});
|
|
17
|
+
it("rejects schemaVersion != 1", () => {
|
|
18
|
+
const r = manifestSchema.safeParse({ ...minimal, schemaVersion: 2 });
|
|
19
|
+
assert.equal(r.success, false);
|
|
20
|
+
});
|
|
21
|
+
it("rejects non-kebab names", () => {
|
|
22
|
+
const r = manifestSchema.safeParse({ ...minimal, name: "Prometheus_X" });
|
|
23
|
+
assert.equal(r.success, false);
|
|
24
|
+
});
|
|
25
|
+
it("rejects non-semver versions", () => {
|
|
26
|
+
const r = manifestSchema.safeParse({ ...minimal, version: "v1.0" });
|
|
27
|
+
assert.equal(r.success, false);
|
|
28
|
+
});
|
|
29
|
+
it("requires at least one signalType", () => {
|
|
30
|
+
const r = manifestSchema.safeParse({ ...minimal, signalTypes: [] });
|
|
31
|
+
assert.equal(r.success, false);
|
|
32
|
+
});
|
|
33
|
+
it("rejects unknown signalType", () => {
|
|
34
|
+
const r = manifestSchema.safeParse({ ...minimal, signalTypes: ["spans"] });
|
|
35
|
+
assert.equal(r.success, false);
|
|
36
|
+
});
|
|
37
|
+
it("accepts a fully-populated manifest", () => {
|
|
38
|
+
const full = {
|
|
39
|
+
...minimal,
|
|
40
|
+
homepage: "https://example.com/connector-prom",
|
|
41
|
+
license: "MIT",
|
|
42
|
+
logo: "./logo.svg",
|
|
43
|
+
configSchema: { type: "object" },
|
|
44
|
+
capabilities: { queryMetrics: true, listServices: true },
|
|
45
|
+
compat: { serverVersion: ">=1.4.0" },
|
|
46
|
+
};
|
|
47
|
+
const r = manifestSchema.safeParse(full);
|
|
48
|
+
assert.equal(r.success, true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { calculateHealthScore } from "../analysis/health.js";
|
|
2
2
|
import { detectRecentAnomaly } from "../analysis/anomaly.js";
|
|
3
|
+
import { sanitizeForLog } from "../util/sanitize.js";
|
|
3
4
|
let _thresholds = null;
|
|
4
5
|
export function setHealthThresholds(t) {
|
|
5
6
|
_thresholds = t;
|
|
@@ -41,7 +42,7 @@ export async function getServiceHealthHandler(registry, args) {
|
|
|
41
42
|
checkAnomaly(latResult.values.map(v => v.value), "latency_p99", args.service, connector.name, anomalies);
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
|
-
console.error(
|
|
45
|
+
console.error("Health check metrics failed for %s:", sanitizeForLog(args.service), err);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
// Gather logs
|
|
@@ -64,7 +65,7 @@ export async function getServiceHealthHandler(registry, args) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
catch (err) {
|
|
67
|
-
console.error(
|
|
68
|
+
console.error("Health check logs failed for %s:", sanitizeForLog(args.service), err);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
// Calculate health score
|