@thotischner/observability-mcp 1.7.1 → 3.0.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/config/products.yaml.example +48 -0
- package/dist/analysis/history.d.ts +70 -0
- package/dist/analysis/history.js +170 -0
- package/dist/analysis/history.test.d.ts +1 -0
- package/dist/analysis/history.test.js +141 -0
- package/dist/audit/log.d.ts +108 -0
- package/dist/audit/log.js +200 -0
- package/dist/audit/log.test.d.ts +1 -0
- package/dist/audit/log.test.js +147 -0
- package/dist/audit/middleware.d.ts +20 -0
- package/dist/audit/middleware.js +50 -0
- package/dist/audit/redaction-bypass.d.ts +67 -0
- package/dist/audit/redaction-bypass.js +64 -0
- package/dist/audit/redaction-bypass.test.d.ts +1 -0
- package/dist/audit/redaction-bypass.test.js +72 -0
- package/dist/audit/sinks/types.d.ts +18 -0
- package/dist/audit/sinks/types.js +1 -0
- package/dist/audit/sinks/webhook.d.ts +45 -0
- package/dist/audit/sinks/webhook.js +111 -0
- package/dist/audit/sinks/webhook.test.d.ts +1 -0
- package/dist/audit/sinks/webhook.test.js +162 -0
- package/dist/auth/credentials.d.ts +29 -0
- package/dist/auth/credentials.js +53 -1
- package/dist/auth/credentials.test.js +46 -1
- package/dist/auth/csrf.d.ts +26 -0
- package/dist/auth/csrf.js +128 -0
- package/dist/auth/csrf.test.d.ts +1 -0
- package/dist/auth/csrf.test.js +143 -0
- package/dist/auth/local-users.d.ts +68 -0
- package/dist/auth/local-users.js +154 -0
- package/dist/auth/local-users.test.d.ts +1 -0
- package/dist/auth/local-users.test.js +121 -0
- package/dist/auth/middleware.d.ts +49 -0
- package/dist/auth/middleware.js +65 -0
- package/dist/auth/middleware.test.d.ts +1 -0
- package/dist/auth/middleware.test.js +90 -0
- package/dist/auth/oidc/client.d.ts +73 -0
- package/dist/auth/oidc/client.js +104 -0
- package/dist/auth/oidc/client.test.d.ts +1 -0
- package/dist/auth/oidc/client.test.js +121 -0
- package/dist/auth/oidc/dcr.d.ts +70 -0
- package/dist/auth/oidc/dcr.js +160 -0
- package/dist/auth/oidc/dcr.test.d.ts +1 -0
- package/dist/auth/oidc/dcr.test.js +109 -0
- package/dist/auth/oidc/discovery.d.ts +38 -0
- package/dist/auth/oidc/discovery.js +48 -0
- package/dist/auth/oidc/discovery.test.d.ts +1 -0
- package/dist/auth/oidc/discovery.test.js +68 -0
- package/dist/auth/oidc/endpoints.d.ts +20 -0
- package/dist/auth/oidc/endpoints.js +168 -0
- package/dist/auth/oidc/endpoints.test.d.ts +7 -0
- package/dist/auth/oidc/endpoints.test.js +304 -0
- package/dist/auth/oidc/flow-cookie.d.ts +57 -0
- package/dist/auth/oidc/flow-cookie.js +142 -0
- package/dist/auth/oidc/flow-cookie.test.d.ts +1 -0
- package/dist/auth/oidc/flow-cookie.test.js +0 -0
- package/dist/auth/oidc/index.d.ts +7 -0
- package/dist/auth/oidc/index.js +6 -0
- package/dist/auth/oidc/jwks.d.ts +36 -0
- package/dist/auth/oidc/jwks.js +69 -0
- package/dist/auth/oidc/jwks.test.d.ts +1 -0
- package/dist/auth/oidc/jwks.test.js +65 -0
- package/dist/auth/oidc/jwt.d.ts +62 -0
- package/dist/auth/oidc/jwt.js +113 -0
- package/dist/auth/oidc/jwt.test.d.ts +1 -0
- package/dist/auth/oidc/jwt.test.js +141 -0
- package/dist/auth/oidc/pkce.d.ts +19 -0
- package/dist/auth/oidc/pkce.js +43 -0
- package/dist/auth/oidc/pkce.test.d.ts +1 -0
- package/dist/auth/oidc/pkce.test.js +55 -0
- package/dist/auth/oidc/profiles.d.ts +22 -0
- package/dist/auth/oidc/profiles.js +95 -0
- package/dist/auth/oidc/profiles.test.d.ts +1 -0
- package/dist/auth/oidc/profiles.test.js +51 -0
- package/dist/auth/oidc/runtime.d.ts +66 -0
- package/dist/auth/oidc/runtime.js +142 -0
- package/dist/auth/oidc/runtime.test.d.ts +1 -0
- package/dist/auth/oidc/runtime.test.js +181 -0
- package/dist/auth/policy/batch-dry-run.d.ts +56 -0
- package/dist/auth/policy/batch-dry-run.js +129 -0
- package/dist/auth/policy/batch-dry-run.test.d.ts +1 -0
- package/dist/auth/policy/batch-dry-run.test.js +140 -0
- package/dist/auth/policy/engine.d.ts +64 -0
- package/dist/auth/policy/engine.js +87 -0
- package/dist/auth/policy/engine.test.d.ts +1 -0
- package/dist/auth/policy/engine.test.js +98 -0
- package/dist/auth/policy/loader.d.ts +45 -0
- package/dist/auth/policy/loader.js +137 -0
- package/dist/auth/policy/loader.test.d.ts +1 -0
- package/dist/auth/policy/loader.test.js +86 -0
- package/dist/auth/policy/opa.d.ts +69 -0
- package/dist/auth/policy/opa.js +173 -0
- package/dist/auth/policy/opa.test.d.ts +1 -0
- package/dist/auth/policy/opa.test.js +206 -0
- package/dist/auth/rbac.d.ts +62 -0
- package/dist/auth/rbac.js +162 -0
- package/dist/auth/rbac.test.d.ts +1 -0
- package/dist/auth/rbac.test.js +183 -0
- package/dist/auth/session.d.ts +66 -0
- package/dist/auth/session.js +146 -0
- package/dist/auth/session.test.d.ts +1 -0
- package/dist/auth/session.test.js +90 -0
- package/dist/catalog/loader.d.ts +67 -0
- package/dist/catalog/loader.js +122 -0
- package/dist/catalog/loader.test.d.ts +1 -0
- package/dist/catalog/loader.test.js +108 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/inspector-config.d.ts +9 -0
- package/dist/cli/inspector-config.js +28 -0
- package/dist/cli/inspector-config.test.d.ts +1 -0
- package/dist/cli/inspector-config.test.js +33 -0
- package/dist/cli/lib.d.ts +1 -1
- package/dist/cli/lib.js +1 -0
- package/dist/conformance/mcp-2025-11-25.test.d.ts +1 -0
- package/dist/conformance/mcp-2025-11-25.test.js +206 -0
- package/dist/connectors/interface.d.ts +5 -1
- package/dist/connectors/loader.js +6 -4
- package/dist/connectors/loader.test.d.ts +1 -0
- package/dist/connectors/loader.test.js +78 -0
- package/dist/connectors/prometheus.test.js +31 -13
- package/dist/connectors/registry.d.ts +13 -0
- package/dist/connectors/registry.js +30 -0
- package/dist/connectors/registry.test.js +56 -2
- package/dist/context.d.ts +45 -1
- package/dist/context.js +40 -1
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +58 -0
- package/dist/federation/registry.d.ts +32 -0
- package/dist/federation/registry.js +77 -0
- package/dist/federation/registry.test.d.ts +1 -0
- package/dist/federation/registry.test.js +130 -0
- package/dist/federation/upstream.d.ts +60 -0
- package/dist/federation/upstream.js +114 -0
- package/dist/index.js +2124 -73
- package/dist/middleware/ssrfGuard.d.ts +15 -0
- package/dist/middleware/ssrfGuard.js +103 -0
- package/dist/middleware/ssrfGuard.test.d.ts +1 -0
- package/dist/middleware/ssrfGuard.test.js +81 -0
- package/dist/net/egress-policy.js +2 -0
- package/dist/observability/otel.d.ts +20 -0
- package/dist/observability/otel.js +118 -0
- package/dist/observability/otel.test.d.ts +1 -0
- package/dist/observability/otel.test.js +56 -0
- package/dist/openapi.js +654 -6
- package/dist/openapi.test.d.ts +1 -0
- package/dist/openapi.test.js +98 -0
- package/dist/policy/redact.d.ts +44 -0
- package/dist/policy/redact.js +144 -0
- package/dist/policy/redact.test.d.ts +1 -0
- package/dist/policy/redact.test.js +172 -0
- package/dist/postmortem/synthesizer.d.ts +83 -0
- package/dist/postmortem/synthesizer.js +205 -0
- package/dist/postmortem/synthesizer.test.d.ts +1 -0
- package/dist/postmortem/synthesizer.test.js +141 -0
- package/dist/products/loader.d.ts +112 -0
- package/dist/products/loader.js +289 -0
- package/dist/products/loader.test.d.ts +1 -0
- package/dist/products/loader.test.js +257 -0
- package/dist/quota/charge.d.ts +28 -0
- package/dist/quota/charge.js +30 -0
- package/dist/quota/charge.test.d.ts +1 -0
- package/dist/quota/charge.test.js +83 -0
- package/dist/quota/limiter.d.ts +97 -0
- package/dist/quota/limiter.js +161 -0
- package/dist/quota/limiter.test.d.ts +1 -0
- package/dist/quota/limiter.test.js +205 -0
- package/dist/quota/token-budget.d.ts +119 -0
- package/dist/quota/token-budget.js +297 -0
- package/dist/quota/token-budget.test.d.ts +1 -0
- package/dist/quota/token-budget.test.js +215 -0
- package/dist/scim/group-role-map.d.ts +4 -0
- package/dist/scim/group-role-map.js +33 -0
- package/dist/scim/group-role-map.test.d.ts +1 -0
- package/dist/scim/group-role-map.test.js +33 -0
- package/dist/scim/routes.d.ts +15 -0
- package/dist/scim/routes.js +249 -0
- package/dist/scim/store.d.ts +37 -0
- package/dist/scim/store.js +178 -0
- package/dist/scim/store.test.d.ts +1 -0
- package/dist/scim/store.test.js +121 -0
- package/dist/scim/types.d.ts +73 -0
- package/dist/scim/types.js +29 -0
- package/dist/sdk/hooks.d.ts +77 -0
- package/dist/sdk/hooks.js +72 -0
- package/dist/sdk/hooks.test.d.ts +1 -0
- package/dist/sdk/hooks.test.js +159 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/manifest-schema.d.ts +17 -0
- package/dist/sdk/manifest-schema.js +21 -0
- package/dist/tenancy/context.d.ts +45 -0
- package/dist/tenancy/context.js +97 -0
- package/dist/tenancy/context.test.d.ts +1 -0
- package/dist/tenancy/context.test.js +72 -0
- package/dist/tenancy/migration.test.d.ts +7 -0
- package/dist/tenancy/migration.test.js +75 -0
- package/dist/tools/context-seam.test.js +6 -1
- package/dist/tools/detect-anomalies.d.ts +1 -1
- package/dist/tools/detect-anomalies.js +5 -4
- package/dist/tools/generate-postmortem.d.ts +35 -0
- package/dist/tools/generate-postmortem.js +191 -0
- package/dist/tools/get-anomaly-history.d.ts +35 -0
- package/dist/tools/get-anomaly-history.js +126 -0
- package/dist/tools/get-service-health.d.ts +1 -1
- package/dist/tools/get-service-health.js +4 -3
- package/dist/tools/list-services.d.ts +1 -1
- package/dist/tools/list-services.js +3 -2
- package/dist/tools/list-sources.d.ts +1 -1
- package/dist/tools/list-sources.js +6 -2
- package/dist/tools/query-logs.d.ts +1 -1
- package/dist/tools/query-logs.js +2 -2
- package/dist/tools/query-metrics.d.ts +1 -1
- package/dist/tools/query-metrics.js +19 -6
- package/dist/tools/query-traces.d.ts +47 -0
- package/dist/tools/query-traces.js +145 -0
- package/dist/tools/query-traces.test.d.ts +1 -0
- package/dist/tools/query-traces.test.js +110 -0
- package/dist/tools/registry-names.d.ts +35 -0
- package/dist/tools/registry-names.js +54 -0
- package/dist/tools/registry-names.test.d.ts +1 -0
- package/dist/tools/registry-names.test.js +61 -0
- package/dist/tools/topology.d.ts +3 -3
- package/dist/tools/topology.js +10 -6
- package/dist/topology/merge.d.ts +22 -0
- package/dist/topology/merge.js +178 -0
- package/dist/topology/merge.test.d.ts +1 -0
- package/dist/topology/merge.test.js +110 -0
- package/dist/transport/sessionStore.d.ts +66 -0
- package/dist/transport/sessionStore.js +138 -0
- package/dist/transport/sessionStore.test.d.ts +1 -0
- package/dist/transport/sessionStore.test.js +118 -0
- package/dist/transport/websocket.d.ts +35 -0
- package/dist/transport/websocket.js +133 -0
- package/dist/transport/websocket.test.d.ts +1 -0
- package/dist/transport/websocket.test.js +124 -0
- package/dist/types.d.ts +51 -0
- package/dist/ui/index.html +3083 -88
- package/package.json +32 -5
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service catalog: an operator-curated map of service name → ownership /
|
|
3
|
+
* criticality / on-call metadata. Hooked into the existing /api/services
|
|
4
|
+
* and /api/health responses so the UI (and the agent through the MCP
|
|
5
|
+
* tools that derive from those endpoints) can see "this is owned by
|
|
6
|
+
* team-payments, criticality tier-1, on-call URL …" without having to
|
|
7
|
+
* cross-reference an external CMDB.
|
|
8
|
+
*
|
|
9
|
+
* On-disk format: JSON file (commit-friendly, easy to diff in PRs). Path
|
|
10
|
+
* defaults to `mcp-server/config/catalog.json`; override via
|
|
11
|
+
* `OMCP_SERVICE_CATALOG_FILE`. Missing or malformed file => no catalog
|
|
12
|
+
* (the server boots fine, enrichment is a no-op).
|
|
13
|
+
*
|
|
14
|
+
* Distinct from the enterprise-gate `OMCP_CATALOG` "product catalog"
|
|
15
|
+
* which lives behind the entitlement gate and governs MCP-tool grants —
|
|
16
|
+
* different trust model, different schema.
|
|
17
|
+
*/
|
|
18
|
+
export type CriticalityTier = "tier-1" | "tier-2" | "tier-3" | "tier-4";
|
|
19
|
+
export type DataClassification = "public" | "internal" | "confidential" | "restricted";
|
|
20
|
+
export interface ServiceCatalogEntry {
|
|
21
|
+
/** Stable short owner identifier — team slug, squad, individual handle. */
|
|
22
|
+
owner?: string;
|
|
23
|
+
/** Human-readable description. One sentence. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Page-the-team URL — Slack channel, on-call rota, runbook index. */
|
|
26
|
+
onCall?: string;
|
|
27
|
+
/** Operator-defined criticality bucket. */
|
|
28
|
+
tier?: CriticalityTier;
|
|
29
|
+
/** Data classification of what flows through the service. */
|
|
30
|
+
dataClassification?: DataClassification;
|
|
31
|
+
/** Free-form SLO label ("99.9% / month" / "99.5%"). Not parsed. */
|
|
32
|
+
slo?: string;
|
|
33
|
+
/** Optional list of relevant runbook URLs. */
|
|
34
|
+
runbooks?: string[];
|
|
35
|
+
/** Optional list of free-form tags. */
|
|
36
|
+
tags?: string[];
|
|
37
|
+
/** Tenant the entry belongs to. Omitted → "default". Used by
|
|
38
|
+
* multi-tenant deployments to scope what /api/catalog and the
|
|
39
|
+
* list_services / get_service_health tools surface per session. */
|
|
40
|
+
tenant?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ServiceCatalog {
|
|
43
|
+
/** Map service name → entry. Service-name key is the same string
|
|
44
|
+
* `list_services` returns; no fuzzy matching. */
|
|
45
|
+
services: Record<string, ServiceCatalogEntry>;
|
|
46
|
+
}
|
|
47
|
+
/** Read + validate a catalog file. Returns the empty catalog on any
|
|
48
|
+
* problem and (when configured) emits a single warn-level console.error
|
|
49
|
+
* so the operator notices but the server keeps booting. */
|
|
50
|
+
export declare function readCatalogFile(path: string | undefined): Promise<ServiceCatalog>;
|
|
51
|
+
/** Pure validator — useful in tests and when feeding in-memory data. */
|
|
52
|
+
export declare function validateCatalog(input: unknown): ServiceCatalog;
|
|
53
|
+
/** Lookup wrapper used by the enricher / API handlers. */
|
|
54
|
+
export declare class CatalogStore {
|
|
55
|
+
private catalog;
|
|
56
|
+
constructor(catalog?: ServiceCatalog);
|
|
57
|
+
/** Lookup. When `tenant` is set, returns undefined for entries
|
|
58
|
+
* belonging to a different tenant — so a cross-tenant
|
|
59
|
+
* enrichment never leaks owner / on-call / SLO bytes. Entries
|
|
60
|
+
* without a tenant field are treated as `"default"`. */
|
|
61
|
+
get(serviceName: string, tenant?: string): ServiceCatalogEntry | undefined;
|
|
62
|
+
/** Snapshot. When `tenant` is set, filters down to entries in that
|
|
63
|
+
* tenant; entries without a tenant field counted as `"default"`. */
|
|
64
|
+
list(tenant?: string): Record<string, ServiceCatalogEntry>;
|
|
65
|
+
count(tenant?: string): number;
|
|
66
|
+
replace(catalog: ServiceCatalog): void;
|
|
67
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service catalog: an operator-curated map of service name → ownership /
|
|
3
|
+
* criticality / on-call metadata. Hooked into the existing /api/services
|
|
4
|
+
* and /api/health responses so the UI (and the agent through the MCP
|
|
5
|
+
* tools that derive from those endpoints) can see "this is owned by
|
|
6
|
+
* team-payments, criticality tier-1, on-call URL …" without having to
|
|
7
|
+
* cross-reference an external CMDB.
|
|
8
|
+
*
|
|
9
|
+
* On-disk format: JSON file (commit-friendly, easy to diff in PRs). Path
|
|
10
|
+
* defaults to `mcp-server/config/catalog.json`; override via
|
|
11
|
+
* `OMCP_SERVICE_CATALOG_FILE`. Missing or malformed file => no catalog
|
|
12
|
+
* (the server boots fine, enrichment is a no-op).
|
|
13
|
+
*
|
|
14
|
+
* Distinct from the enterprise-gate `OMCP_CATALOG` "product catalog"
|
|
15
|
+
* which lives behind the entitlement gate and governs MCP-tool grants —
|
|
16
|
+
* different trust model, different schema.
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from "node:fs";
|
|
19
|
+
const EMPTY_CATALOG = { services: {} };
|
|
20
|
+
const VALID_TIERS = new Set(["tier-1", "tier-2", "tier-3", "tier-4"]);
|
|
21
|
+
const VALID_CLASS = new Set(["public", "internal", "confidential", "restricted"]);
|
|
22
|
+
/** Read + validate a catalog file. Returns the empty catalog on any
|
|
23
|
+
* problem and (when configured) emits a single warn-level console.error
|
|
24
|
+
* so the operator notices but the server keeps booting. */
|
|
25
|
+
export async function readCatalogFile(path) {
|
|
26
|
+
if (!path)
|
|
27
|
+
return EMPTY_CATALOG;
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = await fs.readFile(path, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return EMPTY_CATALOG;
|
|
34
|
+
}
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.error(`[catalog] OMCP_SERVICE_CATALOG_FILE=${path} is not valid JSON: ${e.message}`);
|
|
41
|
+
return EMPTY_CATALOG;
|
|
42
|
+
}
|
|
43
|
+
return validateCatalog(parsed);
|
|
44
|
+
}
|
|
45
|
+
/** Pure validator — useful in tests and when feeding in-memory data. */
|
|
46
|
+
export function validateCatalog(input) {
|
|
47
|
+
if (!input || typeof input !== "object")
|
|
48
|
+
return EMPTY_CATALOG;
|
|
49
|
+
const obj = input;
|
|
50
|
+
const rawServices = obj.services;
|
|
51
|
+
if (!rawServices || typeof rawServices !== "object")
|
|
52
|
+
return EMPTY_CATALOG;
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [name, value] of Object.entries(rawServices)) {
|
|
55
|
+
if (!value || typeof value !== "object")
|
|
56
|
+
continue;
|
|
57
|
+
const e = value;
|
|
58
|
+
const entry = {};
|
|
59
|
+
if (typeof e.owner === "string")
|
|
60
|
+
entry.owner = e.owner;
|
|
61
|
+
if (typeof e.description === "string")
|
|
62
|
+
entry.description = e.description;
|
|
63
|
+
if (typeof e.onCall === "string")
|
|
64
|
+
entry.onCall = e.onCall;
|
|
65
|
+
if (typeof e.tier === "string" && VALID_TIERS.has(e.tier)) {
|
|
66
|
+
entry.tier = e.tier;
|
|
67
|
+
}
|
|
68
|
+
if (typeof e.dataClassification === "string" && VALID_CLASS.has(e.dataClassification)) {
|
|
69
|
+
entry.dataClassification = e.dataClassification;
|
|
70
|
+
}
|
|
71
|
+
if (typeof e.slo === "string")
|
|
72
|
+
entry.slo = e.slo;
|
|
73
|
+
if (Array.isArray(e.runbooks)) {
|
|
74
|
+
entry.runbooks = e.runbooks.filter((x) => typeof x === "string");
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(e.tags)) {
|
|
77
|
+
entry.tags = e.tags.filter((x) => typeof x === "string");
|
|
78
|
+
}
|
|
79
|
+
if (typeof e.tenant === "string")
|
|
80
|
+
entry.tenant = e.tenant;
|
|
81
|
+
out[name] = entry;
|
|
82
|
+
}
|
|
83
|
+
return { services: out };
|
|
84
|
+
}
|
|
85
|
+
/** Lookup wrapper used by the enricher / API handlers. */
|
|
86
|
+
export class CatalogStore {
|
|
87
|
+
catalog;
|
|
88
|
+
constructor(catalog = EMPTY_CATALOG) {
|
|
89
|
+
this.catalog = catalog;
|
|
90
|
+
}
|
|
91
|
+
/** Lookup. When `tenant` is set, returns undefined for entries
|
|
92
|
+
* belonging to a different tenant — so a cross-tenant
|
|
93
|
+
* enrichment never leaks owner / on-call / SLO bytes. Entries
|
|
94
|
+
* without a tenant field are treated as `"default"`. */
|
|
95
|
+
get(serviceName, tenant) {
|
|
96
|
+
const e = this.catalog.services[serviceName];
|
|
97
|
+
if (!e)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (!tenant)
|
|
100
|
+
return e;
|
|
101
|
+
const entryTenant = e.tenant || "default";
|
|
102
|
+
return entryTenant === tenant ? e : undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Snapshot. When `tenant` is set, filters down to entries in that
|
|
105
|
+
* tenant; entries without a tenant field counted as `"default"`. */
|
|
106
|
+
list(tenant) {
|
|
107
|
+
if (!tenant)
|
|
108
|
+
return this.catalog.services;
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const [k, v] of Object.entries(this.catalog.services)) {
|
|
111
|
+
if ((v.tenant || "default") === tenant)
|
|
112
|
+
out[k] = v;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
count(tenant) {
|
|
117
|
+
return Object.keys(this.list(tenant)).length;
|
|
118
|
+
}
|
|
119
|
+
replace(catalog) {
|
|
120
|
+
this.catalog = catalog;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { readCatalogFile, validateCatalog, CatalogStore } from "./loader.js";
|
|
7
|
+
test("readCatalogFile — missing path returns empty catalog", async () => {
|
|
8
|
+
const c = await readCatalogFile(undefined);
|
|
9
|
+
assert.deepEqual(c, { services: {} });
|
|
10
|
+
});
|
|
11
|
+
test("readCatalogFile — missing file returns empty catalog (no crash)", async () => {
|
|
12
|
+
const c = await readCatalogFile("/tmp/definitely-does-not-exist-omcp.json");
|
|
13
|
+
assert.deepEqual(c, { services: {} });
|
|
14
|
+
});
|
|
15
|
+
test("readCatalogFile — malformed JSON returns empty catalog", async () => {
|
|
16
|
+
const dir = await mkdtemp(join(tmpdir(), "omcp-catalog-"));
|
|
17
|
+
try {
|
|
18
|
+
const file = join(dir, "catalog.json");
|
|
19
|
+
await writeFile(file, "{this is not json", "utf8");
|
|
20
|
+
const c = await readCatalogFile(file);
|
|
21
|
+
assert.deepEqual(c, { services: {} });
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await rm(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
test("readCatalogFile — valid file returns parsed catalog", async () => {
|
|
28
|
+
const dir = await mkdtemp(join(tmpdir(), "omcp-catalog-"));
|
|
29
|
+
try {
|
|
30
|
+
const file = join(dir, "catalog.json");
|
|
31
|
+
await writeFile(file, JSON.stringify({
|
|
32
|
+
services: {
|
|
33
|
+
"payment-service": {
|
|
34
|
+
owner: "team-payments",
|
|
35
|
+
tier: "tier-1",
|
|
36
|
+
dataClassification: "confidential",
|
|
37
|
+
slo: "99.9%",
|
|
38
|
+
runbooks: ["https://runbooks.example/payments"],
|
|
39
|
+
tags: ["pci", "regulated"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}), "utf8");
|
|
43
|
+
const c = await readCatalogFile(file);
|
|
44
|
+
assert.equal(c.services["payment-service"].owner, "team-payments");
|
|
45
|
+
assert.equal(c.services["payment-service"].tier, "tier-1");
|
|
46
|
+
assert.deepEqual(c.services["payment-service"].tags, ["pci", "regulated"]);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await rm(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
test("validateCatalog — rejects invalid tier values", () => {
|
|
53
|
+
const c = validateCatalog({ services: { svc: { tier: "tier-9" } } });
|
|
54
|
+
assert.equal(c.services.svc.tier, undefined);
|
|
55
|
+
});
|
|
56
|
+
test("validateCatalog — rejects invalid data classification", () => {
|
|
57
|
+
const c = validateCatalog({ services: { svc: { dataClassification: "ultra-secret" } } });
|
|
58
|
+
assert.equal(c.services.svc.dataClassification, undefined);
|
|
59
|
+
});
|
|
60
|
+
test("validateCatalog — strips non-string entries from runbooks / tags", () => {
|
|
61
|
+
const c = validateCatalog({
|
|
62
|
+
services: { svc: { runbooks: ["a", 1, "b", null], tags: [{}, "tag1"] } },
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(c.services.svc.runbooks, ["a", "b"]);
|
|
65
|
+
assert.deepEqual(c.services.svc.tags, ["tag1"]);
|
|
66
|
+
});
|
|
67
|
+
test("validateCatalog — skips non-object service entries", () => {
|
|
68
|
+
const c = validateCatalog({ services: { svc1: "string", svc2: null, svc3: { owner: "team" } } });
|
|
69
|
+
assert.equal(c.services.svc1, undefined);
|
|
70
|
+
assert.equal(c.services.svc2, undefined);
|
|
71
|
+
assert.equal(c.services.svc3.owner, "team");
|
|
72
|
+
});
|
|
73
|
+
test("CatalogStore — get / list / count / replace", () => {
|
|
74
|
+
const store = new CatalogStore({
|
|
75
|
+
services: {
|
|
76
|
+
a: { owner: "team-a" },
|
|
77
|
+
b: { owner: "team-b" },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
assert.equal(store.count(), 2);
|
|
81
|
+
assert.equal(store.get("a")?.owner, "team-a");
|
|
82
|
+
assert.equal(store.get("nope"), undefined);
|
|
83
|
+
assert.equal(Object.keys(store.list()).length, 2);
|
|
84
|
+
store.replace({ services: { x: { owner: "team-x" } } });
|
|
85
|
+
assert.equal(store.count(), 1);
|
|
86
|
+
assert.equal(store.get("x")?.owner, "team-x");
|
|
87
|
+
});
|
|
88
|
+
test("CatalogStore — tenant filter scopes get / list / count", () => {
|
|
89
|
+
const store = new CatalogStore({
|
|
90
|
+
services: {
|
|
91
|
+
"acme-payments": { owner: "team-a", tenant: "acme" },
|
|
92
|
+
"bigco-payments": { owner: "team-b", tenant: "bigco" },
|
|
93
|
+
"shared-cdn": { owner: "team-c" }, // no tenant → "default"
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
// No tenant filter → see everything (admin-style read)
|
|
97
|
+
assert.equal(store.count(), 3);
|
|
98
|
+
// Tenant-scoped: each sees its own entries + nothing else
|
|
99
|
+
assert.equal(store.count("acme"), 1);
|
|
100
|
+
assert.equal(store.get("acme-payments", "acme")?.owner, "team-a");
|
|
101
|
+
assert.equal(store.get("bigco-payments", "acme"), undefined, "cross-tenant get must return undefined");
|
|
102
|
+
assert.equal(Object.keys(store.list("acme"))[0], "acme-payments");
|
|
103
|
+
// Default tenant includes entries with no tenant field
|
|
104
|
+
assert.equal(store.count("default"), 1);
|
|
105
|
+
assert.equal(store.get("shared-cdn", "default")?.owner, "team-c");
|
|
106
|
+
// Unknown tenant → empty
|
|
107
|
+
assert.equal(store.count("unknown"), 0);
|
|
108
|
+
});
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parseArgs, pickFreePort, composeOverride, resolveCatalogSource, formatPluginList, formatPluginInfo, resolveInstall, splitPassthrough, helmReleaseArgs, HELM_REPO_NAME, HELM_REPO_URL, HELP, } from "./lib.js";
|
|
9
9
|
import { loadTrustRoot, verifyIntegrity, verifyManifestSignature, PluginVerificationError, } from "../connectors/verify.js";
|
|
10
|
+
import { inspectorConfigCommand } from "./inspector-config.js";
|
|
10
11
|
function pkgVersion() {
|
|
11
12
|
try {
|
|
12
13
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -363,6 +364,8 @@ async function main() {
|
|
|
363
364
|
return plugin(sub, positionals, flags);
|
|
364
365
|
case "helm":
|
|
365
366
|
return helm(sub, positionals[0] ?? "observability-mcp", passthrough);
|
|
367
|
+
case "inspector-config":
|
|
368
|
+
return inspectorConfigCommand();
|
|
366
369
|
default:
|
|
367
370
|
fail(`unknown command: ${command}\n\n${HELP}`);
|
|
368
371
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface InspectorConfig {
|
|
2
|
+
mcpServers: Record<string, {
|
|
3
|
+
url: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
}>;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildInspectorConfig(env?: NodeJS.ProcessEnv): InspectorConfig;
|
|
8
|
+
/** CLI entrypoint. Prints JSON to stdout; exits 0 on success. */
|
|
9
|
+
export declare function inspectorConfigCommand(): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `omcp inspector-config` — emit a config JSON the official MCP
|
|
2
|
+
// Inspector can consume. Reads OMCP_BASE_URL (default
|
|
3
|
+
// http://localhost:3000) and optionally OMCP_INSPECTOR_TOKEN to put
|
|
4
|
+
// in the Authorization header.
|
|
5
|
+
//
|
|
6
|
+
// Pipe straight into Inspector:
|
|
7
|
+
// npx @modelcontextprotocol/inspector --config <(omcp inspector-config)
|
|
8
|
+
//
|
|
9
|
+
// Or write to a file:
|
|
10
|
+
// omcp inspector-config > inspector.json
|
|
11
|
+
// npx @modelcontextprotocol/inspector --config inspector.json
|
|
12
|
+
export function buildInspectorConfig(env = process.env) {
|
|
13
|
+
const baseRaw = env.OMCP_BASE_URL?.trim() || "http://localhost:3000";
|
|
14
|
+
const base = baseRaw.replace(/\/$/, "");
|
|
15
|
+
const url = `${base}/mcp`;
|
|
16
|
+
const token = env.OMCP_INSPECTOR_TOKEN?.trim();
|
|
17
|
+
const name = env.OMCP_INSPECTOR_SERVER_NAME?.trim() || "observability-mcp";
|
|
18
|
+
const server = { url };
|
|
19
|
+
if (token) {
|
|
20
|
+
server.headers = { Authorization: `Bearer ${token}` };
|
|
21
|
+
}
|
|
22
|
+
return { mcpServers: { [name]: server } };
|
|
23
|
+
}
|
|
24
|
+
/** CLI entrypoint. Prints JSON to stdout; exits 0 on success. */
|
|
25
|
+
export function inspectorConfigCommand() {
|
|
26
|
+
const cfg = buildInspectorConfig();
|
|
27
|
+
process.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildInspectorConfig } from "./inspector-config.js";
|
|
4
|
+
test("buildInspectorConfig: defaults to localhost:3000/mcp, no headers, observability-mcp server name", () => {
|
|
5
|
+
const cfg = buildInspectorConfig({});
|
|
6
|
+
assert.deepEqual(cfg, {
|
|
7
|
+
mcpServers: {
|
|
8
|
+
"observability-mcp": {
|
|
9
|
+
url: "http://localhost:3000/mcp",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
test("buildInspectorConfig: trims trailing slash from base URL", () => {
|
|
15
|
+
const cfg = buildInspectorConfig({ OMCP_BASE_URL: "https://gw.example.com/" });
|
|
16
|
+
assert.equal(cfg.mcpServers["observability-mcp"].url, "https://gw.example.com/mcp");
|
|
17
|
+
});
|
|
18
|
+
test("buildInspectorConfig: token populates Authorization Bearer", () => {
|
|
19
|
+
const cfg = buildInspectorConfig({
|
|
20
|
+
OMCP_INSPECTOR_TOKEN: "tok-abc",
|
|
21
|
+
});
|
|
22
|
+
assert.equal(cfg.mcpServers["observability-mcp"].headers?.Authorization, "Bearer tok-abc");
|
|
23
|
+
});
|
|
24
|
+
test("buildInspectorConfig: custom server name", () => {
|
|
25
|
+
const cfg = buildInspectorConfig({
|
|
26
|
+
OMCP_INSPECTOR_SERVER_NAME: "my-gateway",
|
|
27
|
+
});
|
|
28
|
+
assert.ok("my-gateway" in cfg.mcpServers);
|
|
29
|
+
});
|
|
30
|
+
test("buildInspectorConfig: empty token trimmed → no headers key", () => {
|
|
31
|
+
const cfg = buildInspectorConfig({ OMCP_INSPECTOR_TOKEN: " " });
|
|
32
|
+
assert.equal(cfg.mcpServers["observability-mcp"].headers, undefined);
|
|
33
|
+
});
|
package/dist/cli/lib.d.ts
CHANGED
|
@@ -92,4 +92,4 @@ export declare function splitPassthrough(argv: string[]): {
|
|
|
92
92
|
};
|
|
93
93
|
/** The helm argv for the install/upgrade step (repo add/update are fixed). */
|
|
94
94
|
export declare function helmReleaseArgs(action: "install" | "upgrade", release: string, passthrough: string[]): string[];
|
|
95
|
-
export declare const HELP = "omcp \u2014 observability-mcp control CLI\n\nUsage:\n omcp version Print CLI + server package version\n omcp doctor Check the local toolchain (docker, compose, helm, node)\n omcp demo up Start the full demo stack (auto-picks free host ports)\n omcp demo down Stop and remove the demo stack\n omcp demo status Show demo container status\n omcp plugin list List connectors from the hub catalog\n omcp plugin info <name> Show one connector's versions + verification info\n omcp plugin install <ref> Install name[@version]: download, verify, extract\n omcp plugin verify <dir> Verify an installed plugin dir against a trust root\n omcp helm install [release] helm repo add+update, then install the signed chart\n omcp helm upgrade [release] Same, as 'helm upgrade --install'\n omcp help Show this help\n\nPass extra helm flags after a literal --, e.g.:\n omcp helm upgrade obs -- -n monitoring --set sources.prometheusUrl=http://prom:9090\n\nFlags:\n --json Machine-readable output (doctor, status, plugin)\n --from <url|path> Catalog source (default: local checkout or the public hub)\n --offline-dir <dir> Airgapped: read <name>-<ver>.tgz[.sig] + manifest from <dir>\n --trust-root <pem> Verify signature+integrity against this PEM (fail-closed)\n --insecure Skip verification (NOT recommended; explicit opt-out)\n --dest <dir> Install target (default: $PLUGINS_DIR or ./plugins)\n --force Overwrite an existing install dir\n";
|
|
95
|
+
export declare const HELP = "omcp \u2014 observability-mcp control CLI\n\nUsage:\n omcp version Print CLI + server package version\n omcp doctor Check the local toolchain (docker, compose, helm, node)\n omcp demo up Start the full demo stack (auto-picks free host ports)\n omcp demo down Stop and remove the demo stack\n omcp demo status Show demo container status\n omcp plugin list List connectors from the hub catalog\n omcp plugin info <name> Show one connector's versions + verification info\n omcp plugin install <ref> Install name[@version]: download, verify, extract\n omcp plugin verify <dir> Verify an installed plugin dir against a trust root\n omcp helm install [release] helm repo add+update, then install the signed chart\n omcp helm upgrade [release] Same, as 'helm upgrade --install'\n omcp inspector-config Print an MCP Inspector config JSON pointing at the local gateway\n omcp help Show this help\n\nPass extra helm flags after a literal --, e.g.:\n omcp helm upgrade obs -- -n monitoring --set sources.prometheusUrl=http://prom:9090\n\nFlags:\n --json Machine-readable output (doctor, status, plugin)\n --from <url|path> Catalog source (default: local checkout or the public hub)\n --offline-dir <dir> Airgapped: read <name>-<ver>.tgz[.sig] + manifest from <dir>\n --trust-root <pem> Verify signature+integrity against this PEM (fail-closed)\n --insecure Skip verification (NOT recommended; explicit opt-out)\n --dest <dir> Install target (default: $PLUGINS_DIR or ./plugins)\n --force Overwrite an existing install dir\n";
|
package/dist/cli/lib.js
CHANGED
|
@@ -169,6 +169,7 @@ Usage:
|
|
|
169
169
|
omcp plugin verify <dir> Verify an installed plugin dir against a trust root
|
|
170
170
|
omcp helm install [release] helm repo add+update, then install the signed chart
|
|
171
171
|
omcp helm upgrade [release] Same, as 'helm upgrade --install'
|
|
172
|
+
omcp inspector-config Print an MCP Inspector config JSON pointing at the local gateway
|
|
172
173
|
omcp help Show this help
|
|
173
174
|
|
|
174
175
|
Pass extra helm flags after a literal --, e.g.:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// MCP 2025-11-25 conformance harness.
|
|
2
|
+
//
|
|
3
|
+
// Run against a running gateway by setting OMCP_CONFORMANCE_URL to
|
|
4
|
+
// its Streamable HTTP endpoint (default http://localhost:3000/mcp).
|
|
5
|
+
// When the env var is unset, every test skips — this lets the suite
|
|
6
|
+
// live in `find src -name "*.test.ts"` without requiring a server
|
|
7
|
+
// during a plain unit-test run.
|
|
8
|
+
//
|
|
9
|
+
// OMCP_CONFORMANCE_URL=http://localhost:3000/mcp \
|
|
10
|
+
// npx tsx --test src/conformance/mcp-2025-11-25.test.ts
|
|
11
|
+
//
|
|
12
|
+
// The `make conformance` target boots the demo stack, waits for
|
|
13
|
+
// /healthz, then runs this file with the URL pointed at the live
|
|
14
|
+
// server.
|
|
15
|
+
import { test } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
const URL_ENV = process.env.OMCP_CONFORMANCE_URL;
|
|
18
|
+
const PROTOCOL_VERSION = "2025-11-25";
|
|
19
|
+
const skip = !URL_ENV;
|
|
20
|
+
const opts = skip ? { skip: "OMCP_CONFORMANCE_URL not set" } : {};
|
|
21
|
+
async function jsonRpc(method, params, opts = {}) {
|
|
22
|
+
if (!URL_ENV)
|
|
23
|
+
throw new Error("OMCP_CONFORMANCE_URL not set");
|
|
24
|
+
const reqHeaders = {
|
|
25
|
+
"content-type": "application/json",
|
|
26
|
+
accept: "application/json, text/event-stream",
|
|
27
|
+
};
|
|
28
|
+
if (opts.session)
|
|
29
|
+
reqHeaders["mcp-session-id"] = opts.session;
|
|
30
|
+
const body = {
|
|
31
|
+
jsonrpc: "2.0",
|
|
32
|
+
id: opts.id ?? 1,
|
|
33
|
+
method,
|
|
34
|
+
params: params ?? {},
|
|
35
|
+
};
|
|
36
|
+
const res = await fetch(URL_ENV, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: reqHeaders,
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
});
|
|
41
|
+
const headers = {};
|
|
42
|
+
res.headers.forEach((v, k) => {
|
|
43
|
+
headers[k] = v;
|
|
44
|
+
});
|
|
45
|
+
// Streamable HTTP may answer with either JSON or SSE; both carry a
|
|
46
|
+
// single JSON-RPC envelope for unary calls. Strip the SSE framing
|
|
47
|
+
// if present so the test only deals with the JSON shape.
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
let response;
|
|
50
|
+
if (text.startsWith("event:") || text.includes("data: ")) {
|
|
51
|
+
const match = text.match(/^data:\s*(.+)$/m);
|
|
52
|
+
response = match ? JSON.parse(match[1]) : {};
|
|
53
|
+
}
|
|
54
|
+
else if (text.trim().startsWith("{")) {
|
|
55
|
+
response = JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
response = {};
|
|
59
|
+
}
|
|
60
|
+
return { response, headers, status: res.status };
|
|
61
|
+
}
|
|
62
|
+
async function notify(method, session) {
|
|
63
|
+
if (!URL_ENV)
|
|
64
|
+
return;
|
|
65
|
+
await fetch(URL_ENV, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
accept: "application/json, text/event-stream",
|
|
70
|
+
"mcp-session-id": session,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ jsonrpc: "2.0", method, params: {} }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function newSession() {
|
|
76
|
+
const { headers, response } = await jsonRpc("initialize", {
|
|
77
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
78
|
+
capabilities: {},
|
|
79
|
+
clientInfo: { name: "conformance-harness", version: "0" },
|
|
80
|
+
}, { id: 1 });
|
|
81
|
+
assert.ok(response.result, "initialize must return a result");
|
|
82
|
+
const session = headers["mcp-session-id"];
|
|
83
|
+
assert.ok(session, "server must issue mcp-session-id on initialize");
|
|
84
|
+
await notify("notifications/initialized", session);
|
|
85
|
+
return session;
|
|
86
|
+
}
|
|
87
|
+
test("MCP 2025-11-25: initialize returns spec-compliant InitializeResult", opts, async () => {
|
|
88
|
+
const { response, headers } = await jsonRpc("initialize", {
|
|
89
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
90
|
+
capabilities: {},
|
|
91
|
+
clientInfo: { name: "harness", version: "0" },
|
|
92
|
+
});
|
|
93
|
+
assert.equal(response.jsonrpc, "2.0");
|
|
94
|
+
assert.equal(response.id, 1);
|
|
95
|
+
assert.ok(response.result && typeof response.result === "object");
|
|
96
|
+
const r = response.result;
|
|
97
|
+
assert.ok(r.protocolVersion, "InitializeResult must include protocolVersion");
|
|
98
|
+
assert.ok(r.capabilities && typeof r.capabilities === "object", "capabilities object required");
|
|
99
|
+
assert.ok(r.serverInfo && typeof r.serverInfo === "object", "serverInfo required");
|
|
100
|
+
assert.ok(r.serverInfo?.name, "serverInfo.name required");
|
|
101
|
+
assert.ok(r.serverInfo?.version, "serverInfo.version required");
|
|
102
|
+
assert.ok(headers["mcp-session-id"], "Mcp-Session-Id header required on initialize response");
|
|
103
|
+
});
|
|
104
|
+
test("MCP 2025-11-25: tools/list returns a Tool[] each with name + inputSchema", opts, async () => {
|
|
105
|
+
const session = await newSession();
|
|
106
|
+
const { response } = await jsonRpc("tools/list", {}, { id: 2, session });
|
|
107
|
+
assert.ok(response.result, JSON.stringify(response.error ?? {}));
|
|
108
|
+
const r = response.result;
|
|
109
|
+
assert.ok(Array.isArray(r.tools), "tools must be an array");
|
|
110
|
+
assert.ok(r.tools && r.tools.length > 0, "gateway must expose at least one tool");
|
|
111
|
+
for (const t of r.tools) {
|
|
112
|
+
assert.ok(t.name && typeof t.name === "string", `tool name required, got ${JSON.stringify(t)}`);
|
|
113
|
+
assert.ok(t.inputSchema && typeof t.inputSchema === "object", `tool ${t.name} missing inputSchema`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
test("MCP 2025-11-25: tools/call dispatches and returns CallToolResult", opts, async () => {
|
|
117
|
+
const session = await newSession();
|
|
118
|
+
const { response } = await jsonRpc("tools/call", { name: "list_sources", arguments: {} }, { id: 3, session });
|
|
119
|
+
// Either a result (success path) or a JSON-RPC error — both are
|
|
120
|
+
// spec-compliant; we just verify shape.
|
|
121
|
+
if (response.error) {
|
|
122
|
+
assert.equal(typeof response.error.code, "number");
|
|
123
|
+
assert.equal(typeof response.error.message, "string");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const r = response.result;
|
|
127
|
+
assert.ok(Array.isArray(r.content), "CallToolResult.content must be an array");
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test("MCP 2025-11-25: unknown method returns -32601 Method not found", opts, async () => {
|
|
131
|
+
const session = await newSession();
|
|
132
|
+
const { response } = await jsonRpc("this/method/does/not/exist", {}, { id: 99, session });
|
|
133
|
+
assert.ok(response.error, "expected an error envelope");
|
|
134
|
+
assert.equal(response.error?.code, -32601, "spec-mandated error code for unknown method");
|
|
135
|
+
});
|
|
136
|
+
test("MCP 2025-11-25: ping returns an empty result", opts, async () => {
|
|
137
|
+
const session = await newSession();
|
|
138
|
+
const { response } = await jsonRpc("ping", {}, { id: 4, session });
|
|
139
|
+
assert.ok(response.result !== undefined, "ping must return a result (may be empty object)");
|
|
140
|
+
});
|
|
141
|
+
test("MCP 2025-11-25: resources/list returns Resource[] or method-not-found", opts, async () => {
|
|
142
|
+
const session = await newSession();
|
|
143
|
+
const { response } = await jsonRpc("resources/list", {}, { id: 5, session });
|
|
144
|
+
if (response.error) {
|
|
145
|
+
assert.equal(response.error.code, -32601, "if not supported, must be -32601");
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const r = response.result;
|
|
149
|
+
assert.ok(Array.isArray(r.resources), "resources must be an array");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
test("MCP 2025-11-25: prompts/list returns Prompt[] or method-not-found", opts, async () => {
|
|
153
|
+
const session = await newSession();
|
|
154
|
+
const { response } = await jsonRpc("prompts/list", {}, { id: 6, session });
|
|
155
|
+
if (response.error) {
|
|
156
|
+
assert.equal(response.error.code, -32601, "if not supported, must be -32601");
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const r = response.result;
|
|
160
|
+
assert.ok(Array.isArray(r.prompts), "prompts must be an array");
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
test("MCP 2025-11-25: logging/setLevel accepts spec levels or method-not-found", opts, async () => {
|
|
164
|
+
const session = await newSession();
|
|
165
|
+
const { response } = await jsonRpc("logging/setLevel", { level: "info" }, { id: 7, session });
|
|
166
|
+
if (response.error) {
|
|
167
|
+
assert.equal(response.error.code, -32601, "if not supported, must be -32601");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Spec says the result is `EmptyResult` — we don't enforce
|
|
171
|
+
// strictly empty (some implementations include diagnostics) but
|
|
172
|
+
// it must be a JSON object.
|
|
173
|
+
assert.ok(typeof response.result === "object");
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
test("MCP 2025-11-25: tools/call with invalid params returns -32602 or isError result", opts, async () => {
|
|
177
|
+
const session = await newSession();
|
|
178
|
+
const { response } = await jsonRpc("tools/call", { name: "list_sources", arguments: { __invalid_arg: { nested: 1 } } }, { id: 8, session });
|
|
179
|
+
// The spec allows either a JSON-RPC error or an isError CallToolResult.
|
|
180
|
+
// We accept either; reject only on a successful non-error result for
|
|
181
|
+
// input that should not validate.
|
|
182
|
+
if (response.error) {
|
|
183
|
+
assert.ok([-32602, -32600].includes(response.error.code) || response.error.code <= -32000);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const r = response.result;
|
|
187
|
+
// list_sources happens to ignore unknown args — that's fine, the
|
|
188
|
+
// spec doesn't require strict input rejection for tools that opt
|
|
189
|
+
// out. Just confirm we got a shape-conformant CallToolResult.
|
|
190
|
+
assert.ok(Array.isArray(r.content));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
test("MCP 2025-11-25: server advertises protocolVersion equal to or newer than 2025-11-25", opts, async () => {
|
|
194
|
+
const { response } = await jsonRpc("initialize", {
|
|
195
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
196
|
+
capabilities: {},
|
|
197
|
+
clientInfo: { name: "harness", version: "0" },
|
|
198
|
+
}, { id: 100 });
|
|
199
|
+
const r = response.result;
|
|
200
|
+
assert.ok(r.protocolVersion, "protocolVersion must be present in InitializeResult");
|
|
201
|
+
// Spec contract: the server picks the highest version it supports
|
|
202
|
+
// that the client also offered, OR returns the highest it knows
|
|
203
|
+
// about and lets the client decide. We just require it's a
|
|
204
|
+
// recognised date-style version string.
|
|
205
|
+
assert.match(r.protocolVersion, /^\d{4}-\d{2}-\d{2}$/, "protocolVersion must be a YYYY-MM-DD date");
|
|
206
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SignalType, ConnectorHealth, ServiceInfo, MetricInfo, MetricQuery, MetricResult, LogQuery, LogResult, SourceConfig, MetricDefinition, Resource, Edge, TopologySnapshot, TopologyChangeListener } from "../types.js";
|
|
1
|
+
import type { SignalType, ConnectorHealth, ServiceInfo, MetricInfo, MetricQuery, MetricResult, LogQuery, LogResult, TraceQuery, TraceResult, SourceConfig, MetricDefinition, Resource, Edge, TopologySnapshot, TopologyChangeListener } from "../types.js";
|
|
2
2
|
export interface ObservabilityConnector {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
readonly type: string;
|
|
@@ -14,6 +14,10 @@ export interface ObservabilityConnector {
|
|
|
14
14
|
listAvailableMetrics?(service: string): Promise<MetricInfo[]>;
|
|
15
15
|
queryMetrics?(params: MetricQuery): Promise<MetricResult>;
|
|
16
16
|
queryLogs?(params: LogQuery): Promise<LogResult>;
|
|
17
|
+
/** Optional traces capability — Tempo / Jaeger / OTLP backends
|
|
18
|
+
* implement this. The MCP `query_traces` tool fans out to every
|
|
19
|
+
* connector that has it. */
|
|
20
|
+
queryTraces?(params: TraceQuery): Promise<TraceResult>;
|
|
17
21
|
/** Current in-memory resource list. Should be O(1) — backed by the watch cache. */
|
|
18
22
|
listResources?(): Promise<Resource[]>;
|
|
19
23
|
/** Current in-memory edge list. Should be O(1) — backed by the watch cache. */
|