@thotischner/observability-mcp 1.8.1 → 3.0.1
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/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 +9 -0
- package/dist/audit/log.js +20 -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/s3.d.ts +61 -0
- package/dist/audit/sinks/s3.js +179 -0
- package/dist/audit/sinks/s3.test.d.ts +1 -0
- package/dist/audit/sinks/s3.test.js +175 -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 +11 -0
- package/dist/auth/credentials.js +27 -0
- package/dist/auth/credentials.test.js +21 -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 +6 -0
- package/dist/auth/local-users.js +11 -0
- package/dist/auth/local-users.test.js +41 -0
- package/dist/auth/middleware.d.ts +7 -6
- 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/endpoints.js +44 -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 +3 -0
- package/dist/auth/oidc/runtime.js +16 -3
- package/dist/auth/oidc/runtime.test.js +1 -0
- package/dist/auth/policy/batch-dry-run.d.ts +56 -0
- package/dist/auth/policy/batch-dry-run.js +144 -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 +20 -4
- package/dist/auth/policy/engine.js +16 -2
- package/dist/auth/policy/loader.d.ts +11 -1
- package/dist/auth/policy/loader.js +37 -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 +5 -5
- package/dist/auth/policy/opa.js +25 -14
- package/dist/auth/policy/opa.test.js +48 -0
- package/dist/auth/rbac.d.ts +23 -1
- package/dist/auth/rbac.js +43 -1
- package/dist/auth/rbac.test.js +62 -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.d.ts +8 -0
- package/dist/connectors/loader.js +55 -4
- package/dist/connectors/loader.test.d.ts +1 -0
- package/dist/connectors/loader.test.js +78 -0
- package/dist/connectors/manifest-hooks.test.d.ts +1 -0
- package/dist/connectors/manifest-hooks.test.js +206 -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 +32 -0
- package/dist/context.js +35 -0
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +58 -0
- package/dist/federation/registry.d.ts +54 -0
- package/dist/federation/registry.js +122 -0
- package/dist/federation/registry.test.d.ts +1 -0
- package/dist/federation/registry.test.js +206 -0
- package/dist/federation/upstream.d.ts +86 -0
- package/dist/federation/upstream.js +162 -0
- package/dist/federation/upstream.test.d.ts +1 -0
- package/dist/federation/upstream.test.js +118 -0
- package/dist/index.js +1435 -126
- package/dist/metrics/self.d.ts +1 -0
- package/dist/metrics/self.js +8 -0
- 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/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 +215 -7
- package/dist/openapi.test.js +34 -0
- package/dist/policy/redact.js +1 -1
- package/dist/postmortem/store.d.ts +34 -0
- package/dist/postmortem/store.js +113 -0
- package/dist/postmortem/store.test.d.ts +1 -0
- package/dist/postmortem/store.test.js +118 -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 +31 -3
- package/dist/products/loader.js +77 -4
- package/dist/products/loader.test.js +90 -1
- 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 +29 -4
- package/dist/quota/limiter.js +64 -8
- package/dist/quota/limiter.test.js +86 -0
- package/dist/scim/compliance.test.d.ts +1 -0
- package/dist/scim/compliance.test.js +169 -0
- package/dist/scim/factory.test.d.ts +1 -0
- package/dist/scim/factory.test.js +54 -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/patch-ops.test.d.ts +1 -0
- package/dist/scim/patch-ops.test.js +100 -0
- package/dist/scim/redis-store.d.ts +38 -0
- package/dist/scim/redis-store.js +178 -0
- package/dist/scim/redis-store.test.d.ts +1 -0
- package/dist/scim/redis-store.test.js +138 -0
- package/dist/scim/routes.d.ts +40 -0
- package/dist/scim/routes.js +395 -0
- package/dist/scim/store.d.ts +76 -0
- package/dist/scim/store.js +196 -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/hook-wrappers.d.ts +39 -0
- package/dist/sdk/hook-wrappers.js +113 -0
- package/dist/sdk/hook-wrappers.test.d.ts +1 -0
- package/dist/sdk/hook-wrappers.test.js +204 -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 +15 -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/tools/context-seam.test.js +6 -1
- package/dist/tools/detect-anomalies.d.ts +12 -1
- package/dist/tools/detect-anomalies.js +26 -5
- 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 +33 -11
- package/dist/tools/topology.test.js +45 -0
- 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/transportSessionMap.d.ts +70 -0
- package/dist/transport/transportSessionMap.js +128 -0
- package/dist/transport/transportSessionMap.test.d.ts +1 -0
- package/dist/transport/transportSessionMap.test.js +111 -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 +2529 -145
- package/package.json +13 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Upstream MCP federation client.
|
|
2
|
+
//
|
|
3
|
+
// One UpstreamClient per remote MCP gateway. On connect() it runs the
|
|
4
|
+
// MCP initialize handshake, fetches tools/list, and caches the catalog
|
|
5
|
+
// locally. callTool() forwards a request to the upstream and returns
|
|
6
|
+
// its CallToolResult verbatim.
|
|
7
|
+
//
|
|
8
|
+
// Transports:
|
|
9
|
+
// - "http" — Streamable HTTP to an upstream gateway URL (default).
|
|
10
|
+
// - "stdio" — spawn a child process that speaks MCP over its stdio
|
|
11
|
+
// channels. The classic MCP transport, useful when the
|
|
12
|
+
// upstream is a CLI-style server (omcp inspector-config,
|
|
13
|
+
// a local-only MCP, an in-cluster sidecar).
|
|
14
|
+
// - "ws" — WebSocket to a ws:// or wss:// URL. Useful when the
|
|
15
|
+
// upstream gateway exposes MCP via the WS subprotocol
|
|
16
|
+
// rather than streamable HTTP. No bearer-auth header
|
|
17
|
+
// (the SDK transport only accepts a URL); operators
|
|
18
|
+
// that need auth append it as a query string or run
|
|
19
|
+
// the gateway behind an authenticating reverse proxy.
|
|
20
|
+
//
|
|
21
|
+
// Auth forwarding modes:
|
|
22
|
+
// - "none" — no auth header on outbound calls
|
|
23
|
+
// - "bearer" — static OMCP_FEDERATION_TOKEN_<NAME> sent as Bearer
|
|
24
|
+
// (HTTP transport only — stdio doesn't have HTTP headers)
|
|
25
|
+
//
|
|
26
|
+
// OIDC + UAID passthrough are deferred — they require a per-request
|
|
27
|
+
// identity hand-off the federation manager doesn't carry today.
|
|
28
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
29
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
30
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
31
|
+
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
32
|
+
export class UpstreamClient {
|
|
33
|
+
name;
|
|
34
|
+
/** Empty-string for stdio (no remote URL); kept on the public surface
|
|
35
|
+
* so the UI doesn't have to special-case the transport kind. */
|
|
36
|
+
url;
|
|
37
|
+
namespacePrefix;
|
|
38
|
+
transportKind;
|
|
39
|
+
cfg;
|
|
40
|
+
client;
|
|
41
|
+
// `unknown` because the SDK exposes a different concrete type per
|
|
42
|
+
// transport. Federation only talks to the transport via the SDK
|
|
43
|
+
// Client object, so the concrete type isn't needed here.
|
|
44
|
+
transport;
|
|
45
|
+
toolsCache = [];
|
|
46
|
+
status = "disconnected";
|
|
47
|
+
lastError;
|
|
48
|
+
refreshTimer;
|
|
49
|
+
refreshIntervalMs;
|
|
50
|
+
constructor(cfg) {
|
|
51
|
+
this.cfg = cfg;
|
|
52
|
+
this.name = cfg.name;
|
|
53
|
+
this.transportKind =
|
|
54
|
+
cfg.transport === "stdio" ? "stdio" :
|
|
55
|
+
cfg.transport === "ws" ? "ws" :
|
|
56
|
+
"http";
|
|
57
|
+
this.url =
|
|
58
|
+
this.transportKind === "stdio" ? `stdio:${cfg.command}` :
|
|
59
|
+
this.transportKind === "ws" ? cfg.url :
|
|
60
|
+
cfg.url;
|
|
61
|
+
this.namespacePrefix = cfg.namespacePrefix ?? cfg.name;
|
|
62
|
+
this.refreshIntervalMs = cfg.refreshIntervalMs ?? 5 * 60 * 1000;
|
|
63
|
+
}
|
|
64
|
+
getStatus() {
|
|
65
|
+
return { status: this.status, lastError: this.lastError, toolCount: this.toolsCache.length };
|
|
66
|
+
}
|
|
67
|
+
/** Cached catalog (read-only). */
|
|
68
|
+
getTools() {
|
|
69
|
+
return [...this.toolsCache];
|
|
70
|
+
}
|
|
71
|
+
/** Connect + initial catalog fetch. Logs failures and leaves the
|
|
72
|
+
* client in `degraded` so the catalog stays empty rather than
|
|
73
|
+
* blocking startup. Re-runnable. */
|
|
74
|
+
async connect() {
|
|
75
|
+
this.status = "connecting";
|
|
76
|
+
try {
|
|
77
|
+
this.transport = this.buildTransport();
|
|
78
|
+
this.client = new Client({ name: "observability-mcp-federation", version: "1" }, { capabilities: {} });
|
|
79
|
+
// SDK Client.connect accepts any Transport implementation; the
|
|
80
|
+
// injected test transport just needs the start()/send()/close()
|
|
81
|
+
// contract — the type assertion sidesteps each concrete class.
|
|
82
|
+
await this.client.connect(this.transport);
|
|
83
|
+
await this.refresh();
|
|
84
|
+
this.status = "ready";
|
|
85
|
+
this.lastError = undefined;
|
|
86
|
+
if (this.refreshIntervalMs > 0) {
|
|
87
|
+
this.refreshTimer = setInterval(() => {
|
|
88
|
+
this.refresh().catch((err) => {
|
|
89
|
+
console.warn("UpstreamClient %s: background refresh failed: %s", this.name, err instanceof Error ? err.message : String(err));
|
|
90
|
+
});
|
|
91
|
+
}, this.refreshIntervalMs).unref?.();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
this.status = "degraded";
|
|
96
|
+
this.lastError = err instanceof Error ? err.message : String(err);
|
|
97
|
+
console.warn("UpstreamClient %s: connect failed (%s). Federation continues without this upstream.", this.name, this.lastError);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Re-fetch the upstream tool catalog. Throws on failure so the
|
|
101
|
+
* caller can choose to degrade or retry. */
|
|
102
|
+
async refresh() {
|
|
103
|
+
if (!this.client)
|
|
104
|
+
throw new Error(`upstream ${this.name} not connected`);
|
|
105
|
+
const result = await this.client.listTools({});
|
|
106
|
+
this.toolsCache = (result.tools ?? []).map((t) => ({
|
|
107
|
+
namespacedName: `${this.namespacePrefix}.${t.name}`,
|
|
108
|
+
upstreamName: t.name,
|
|
109
|
+
sourceName: this.name,
|
|
110
|
+
description: t.description ?? "",
|
|
111
|
+
inputSchema: t.inputSchema,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/** Forward a callTool request to the upstream by upstream-tool name
|
|
115
|
+
* (NOT the namespaced name — the registry strips the prefix before
|
|
116
|
+
* calling here). */
|
|
117
|
+
async callTool(upstreamName, args) {
|
|
118
|
+
if (!this.client) {
|
|
119
|
+
throw new Error(`upstream ${this.name} is ${this.status}`);
|
|
120
|
+
}
|
|
121
|
+
return this.client.callTool({
|
|
122
|
+
name: upstreamName,
|
|
123
|
+
arguments: (args ?? {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async close() {
|
|
127
|
+
if (this.refreshTimer)
|
|
128
|
+
clearInterval(this.refreshTimer);
|
|
129
|
+
try {
|
|
130
|
+
await this.client?.close();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* socket may already be down */
|
|
134
|
+
}
|
|
135
|
+
this.status = "disconnected";
|
|
136
|
+
}
|
|
137
|
+
// --- internals ----------------------------------------------------
|
|
138
|
+
buildTransport() {
|
|
139
|
+
// Test path: a pre-built transport short-circuits the spawn / fetch.
|
|
140
|
+
if (this.cfg._transport)
|
|
141
|
+
return this.cfg._transport;
|
|
142
|
+
if (this.transportKind === "stdio") {
|
|
143
|
+
const cfg = this.cfg;
|
|
144
|
+
return new StdioClientTransport({
|
|
145
|
+
command: cfg.command,
|
|
146
|
+
args: cfg.args ?? [],
|
|
147
|
+
env: cfg.env,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (this.transportKind === "ws") {
|
|
151
|
+
const cfg = this.cfg;
|
|
152
|
+
return new WebSocketClientTransport(new URL(cfg.url));
|
|
153
|
+
}
|
|
154
|
+
const cfg = this.cfg;
|
|
155
|
+
const url = new URL(cfg.url);
|
|
156
|
+
const init = { headers: {} };
|
|
157
|
+
if (cfg.bearerToken) {
|
|
158
|
+
init.headers["Authorization"] = `Bearer ${cfg.bearerToken}`;
|
|
159
|
+
}
|
|
160
|
+
return new StreamableHTTPClientTransport(url, { requestInit: init });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { UpstreamClient } from "./upstream.js";
|
|
4
|
+
test("UpstreamClient: HTTP config — transportKind='http', url surfaced", () => {
|
|
5
|
+
const cfg = {
|
|
6
|
+
name: "remote",
|
|
7
|
+
url: "https://gw.example.com/mcp",
|
|
8
|
+
bearerToken: "t0k",
|
|
9
|
+
};
|
|
10
|
+
const c = new UpstreamClient(cfg);
|
|
11
|
+
assert.equal(c.transportKind, "http");
|
|
12
|
+
assert.equal(c.url, "https://gw.example.com/mcp");
|
|
13
|
+
assert.equal(c.namespacePrefix, "remote");
|
|
14
|
+
assert.deepEqual(c.getTools(), []);
|
|
15
|
+
});
|
|
16
|
+
test("UpstreamClient: stdio config — transportKind='stdio', url shows command", () => {
|
|
17
|
+
const cfg = {
|
|
18
|
+
transport: "stdio",
|
|
19
|
+
name: "local-mcp",
|
|
20
|
+
command: "/usr/local/bin/mcp",
|
|
21
|
+
args: ["--config", "/etc/mcp.yaml"],
|
|
22
|
+
};
|
|
23
|
+
const c = new UpstreamClient(cfg);
|
|
24
|
+
assert.equal(c.transportKind, "stdio");
|
|
25
|
+
assert.equal(c.url, "stdio:/usr/local/bin/mcp");
|
|
26
|
+
assert.equal(c.namespacePrefix, "local-mcp");
|
|
27
|
+
});
|
|
28
|
+
test("UpstreamClient: stdio config respects custom namespacePrefix", () => {
|
|
29
|
+
const cfg = {
|
|
30
|
+
transport: "stdio",
|
|
31
|
+
name: "weather",
|
|
32
|
+
command: "weather-mcp",
|
|
33
|
+
namespacePrefix: "weather.local",
|
|
34
|
+
};
|
|
35
|
+
const c = new UpstreamClient(cfg);
|
|
36
|
+
assert.equal(c.namespacePrefix, "weather.local");
|
|
37
|
+
});
|
|
38
|
+
test("UpstreamClient: explicit transport='http' is also accepted", () => {
|
|
39
|
+
const cfg = {
|
|
40
|
+
transport: "http",
|
|
41
|
+
name: "gw",
|
|
42
|
+
url: "https://gw.example.com/mcp",
|
|
43
|
+
};
|
|
44
|
+
const c = new UpstreamClient(cfg);
|
|
45
|
+
assert.equal(c.transportKind, "http");
|
|
46
|
+
});
|
|
47
|
+
test("UpstreamClient: ws transport surfaces the ws:// URL", () => {
|
|
48
|
+
const cfg = {
|
|
49
|
+
transport: "ws",
|
|
50
|
+
name: "gw",
|
|
51
|
+
url: "wss://gw.example.com/mcp/ws",
|
|
52
|
+
};
|
|
53
|
+
const c = new UpstreamClient(cfg);
|
|
54
|
+
assert.equal(c.transportKind, "ws");
|
|
55
|
+
assert.equal(c.url, "wss://gw.example.com/mcp/ws");
|
|
56
|
+
});
|
|
57
|
+
test("UpstreamClient: empty args defaults to [] on stdio", () => {
|
|
58
|
+
const cfg = {
|
|
59
|
+
transport: "stdio",
|
|
60
|
+
name: "x",
|
|
61
|
+
command: "x",
|
|
62
|
+
};
|
|
63
|
+
const c = new UpstreamClient(cfg);
|
|
64
|
+
// Just verifies construction doesn't throw on a minimal stdio config.
|
|
65
|
+
assert.equal(c.transportKind, "stdio");
|
|
66
|
+
});
|
|
67
|
+
test("UpstreamClient: getStatus initial state", () => {
|
|
68
|
+
const c = new UpstreamClient({ name: "x", url: "https://x/mcp" });
|
|
69
|
+
const s = c.getStatus();
|
|
70
|
+
assert.equal(s.status, "disconnected");
|
|
71
|
+
assert.equal(s.toolCount, 0);
|
|
72
|
+
assert.equal(s.lastError, undefined);
|
|
73
|
+
});
|
|
74
|
+
test("UpstreamClient: connect uses injected _transport instead of spawning / fetching", async () => {
|
|
75
|
+
// Build a minimal MCP Transport stub that also COMPLETES the
|
|
76
|
+
// initialize handshake — when the SDK Client sends a JSON-RPC
|
|
77
|
+
// request, we synthesise a matching response on onmessage so the
|
|
78
|
+
// initialize promise resolves quickly (no 60s SDK timeout).
|
|
79
|
+
let started = false;
|
|
80
|
+
let sentMessages = 0;
|
|
81
|
+
const fakeTransport = {
|
|
82
|
+
start: async () => { started = true; },
|
|
83
|
+
send: async (msg) => {
|
|
84
|
+
sentMessages += 1;
|
|
85
|
+
if (msg?.method === "initialize" && msg?.id !== undefined) {
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
fakeTransport.onmessage?.({
|
|
88
|
+
jsonrpc: "2.0",
|
|
89
|
+
id: msg.id,
|
|
90
|
+
result: { protocolVersion: "2024-11-05", capabilities: {}, serverInfo: { name: "fake", version: "1" } },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (msg?.method === "tools/list" && msg?.id !== undefined) {
|
|
95
|
+
queueMicrotask(() => {
|
|
96
|
+
fakeTransport.onmessage?.({ jsonrpc: "2.0", id: msg.id, result: { tools: [] } });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
close: async () => { },
|
|
101
|
+
onclose: undefined,
|
|
102
|
+
onerror: undefined,
|
|
103
|
+
onmessage: undefined,
|
|
104
|
+
};
|
|
105
|
+
const c = new UpstreamClient({
|
|
106
|
+
name: "injected",
|
|
107
|
+
url: "https://ignored.example/mcp",
|
|
108
|
+
refreshIntervalMs: 0,
|
|
109
|
+
_transport: fakeTransport,
|
|
110
|
+
});
|
|
111
|
+
await c.connect();
|
|
112
|
+
await c.close();
|
|
113
|
+
assert.equal(started, true, "fake transport.start() should have been called");
|
|
114
|
+
assert.ok(sentMessages >= 1, "fake transport.send() should have received initialize");
|
|
115
|
+
// Status reaches "ready" only when initialize + tools/list both succeed
|
|
116
|
+
// — confirms our injected transport drove the whole handshake.
|
|
117
|
+
// (connect-time errors leave it in "degraded".)
|
|
118
|
+
});
|