merge: v2/1563-node-proxy (#1590)
This commit is contained in:
commit
4e3cd591a4
10 changed files with 225 additions and 18 deletions
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '22.x'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies (root + all clients)
|
- name: Install dependencies (root + all clients)
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --tr
|
||||||
|
|
||||||
When a server is loaded from a `--catalog`/`--config` file, its per-server settings (headers, connection/request timeouts, and OAuth) are applied to the connection — the same resolution the TUI uses. A `--header` flag overrides the file's headers for that run while leaving the file's timeouts and OAuth in place.
|
When a server is loaded from a `--catalog`/`--config` file, its per-server settings (headers, connection/request timeouts, and OAuth) are applied to the connection — the same resolution the TUI uses. A `--header` flag overrides the file's headers for that run while leaving the file's timeouts and OAuth in place.
|
||||||
|
|
||||||
|
### HTTP proxy support
|
||||||
|
|
||||||
|
Connections to remote HTTP/SSE servers honor the conventional proxy environment variables: `HTTPS_PROXY` / `HTTP_PROXY` (and their lowercase forms) select the proxy, and `NO_PROXY` exempts hosts. This applies to the Node transport shared by the CLI and the web backend — no inspector-specific flag is needed. When a proxy variable is set, outbound requests are routed through undici's `EnvHttpProxyAgent`.
|
||||||
|
|
||||||
|
Proxy routing is powered by the [`undici`](https://www.npmjs.com/package/undici) package (`^8.5.0`, which requires Node `>= 22.19.0` — the inspector's supported floor). It is imported lazily only when a proxy variable is set, so runs without a proxy configured pay no cost.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
### MCP server (which server to connect to)
|
### MCP server (which server to connect to)
|
||||||
|
|
@ -115,23 +121,23 @@ The CLI does **not** start a local callback server or retry connect on 401. If t
|
||||||
|
|
||||||
#### OAuth callback URL
|
#### OAuth callback URL
|
||||||
|
|
||||||
| Surface | Default callback |
|
| Surface | Default callback |
|
||||||
| ------- | ---------------- |
|
| ------- | ----------------------------------------------------------------------------------------- |
|
||||||
| **Web** | `http://localhost:6274/oauth/callback` |
|
| **Web** | `http://localhost:6274/oauth/callback` |
|
||||||
| **TUI** | `http://127.0.0.1:6276/oauth/callback` (interactive — callback server) |
|
| **TUI** | `http://127.0.0.1:6276/oauth/callback` (interactive — callback server) |
|
||||||
| **CLI** | `http://127.0.0.1:6276/oauth/callback` (redirect URI in OAuth metadata only; no listener) |
|
| **CLI** | `http://127.0.0.1:6276/oauth/callback` (redirect URI in OAuth metadata only; no listener) |
|
||||||
|
|
||||||
Register `http://127.0.0.1:6276/oauth/callback` on static or enterprise IdPs that require pre-registered redirect URIs before using the **TUI** (or when your OAuth app expects that URI). Override with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`. The CLI passes this value as `redirect_uri` when an OAuth flow runs, but does not listen on the port.
|
Register `http://127.0.0.1:6276/oauth/callback` on static or enterprise IdPs that require pre-registered redirect URIs before using the **TUI** (or when your OAuth app expects that URI). Override with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`. The CLI passes this value as `redirect_uri` when an OAuth flow runs, but does not listen on the port.
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
|
|
||||||
| Option | Env | Description |
|
| Option | Env | Description |
|
||||||
| ------ | --- | ----------- |
|
| ----------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||||
| `--client-config <path>` | `MCP_CLIENT_CONFIG_PATH` | Install-level client config (default: `~/.mcp-inspector/storage/client.json`). |
|
| `--client-config <path>` | `MCP_CLIENT_CONFIG_PATH` | Install-level client config (default: `~/.mcp-inspector/storage/client.json`). |
|
||||||
| `--client-id <id>` | — | OAuth client ID (static client); overrides `client.json`. |
|
| `--client-id <id>` | — | OAuth client ID (static client); overrides `client.json`. |
|
||||||
| `--client-secret <secret>` | — | OAuth client secret; overrides `client.json`. |
|
| `--client-secret <secret>` | — | OAuth client secret; overrides `client.json`. |
|
||||||
| `--client-metadata-url <url>` | — | CIMD metadata URL; overrides `client.json`. |
|
| `--client-metadata-url <url>` | — | CIMD metadata URL; overrides `client.json`. |
|
||||||
| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | Redirect URI sent to the authorization server (default: `http://127.0.0.1:6276/oauth/callback`). |
|
| `--callback-url <url>` | `MCP_OAUTH_CALLBACK_URL` | Redirect URI sent to the authorization server (default: `http://127.0.0.1:6276/oauth/callback`). |
|
||||||
|
|
||||||
**Example** — list tools on an OAuth-protected server using stored tokens and CIMD from the command line:
|
**Example** — list tools on an OAuth-protected server using stored tokens and CIMD from the command line:
|
||||||
|
|
||||||
|
|
|
||||||
10
clients/cli/package-lock.json
generated
10
clients/cli/package-lock.json
generated
|
|
@ -15,6 +15,7 @@
|
||||||
"atomically": "^2.1.1",
|
"atomically": "^2.1.1",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
|
"undici": "^8.5.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.13"
|
"zustand": "^5.0.13"
|
||||||
},
|
},
|
||||||
|
|
@ -5378,6 +5379,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "8.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz",
|
||||||
|
"integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.18.2",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
"atomically": "^2.1.1",
|
"atomically": "^2.1.1",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
|
"undici": "^8.5.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.13"
|
"zustand": "^5.0.13"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,7 @@ export default defineConfig([
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## HTTP proxy support
|
||||||
|
|
||||||
|
The web backend connects to remote MCP servers through the shared Node transport (`core/mcp/node/transport.ts`), which honors the conventional proxy environment variables: `HTTPS_PROXY` / `HTTP_PROXY` (and their lowercase forms) select the proxy, and `NO_PROXY` exempts hosts. Routing is powered by [`undici`](https://www.npmjs.com/package/undici)'s `EnvHttpProxyAgent`, imported lazily only when a proxy variable is set, so runs without a proxy configured pay no cost. See the CLI README for more detail.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { afterEach, describe, it, expect, vi } from "vitest";
|
||||||
import { getServerType } from "@inspector/core/mcp/config.js";
|
import { getServerType } from "@inspector/core/mcp/config.js";
|
||||||
import { createTransportNode } from "@inspector/core/mcp/node/transport.js";
|
import {
|
||||||
|
createTransportNode,
|
||||||
|
readProxyEnv,
|
||||||
|
withProxyDispatcher,
|
||||||
|
} from "@inspector/core/mcp/node/transport.js";
|
||||||
import type {
|
import type {
|
||||||
InspectorServerSettings,
|
InspectorServerSettings,
|
||||||
MCPServerConfig,
|
MCPServerConfig,
|
||||||
|
|
@ -327,4 +331,111 @@ describe("Transport", () => {
|
||||||
expect(result.transport).toBeDefined();
|
expect(result.transport).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("HTTPS_PROXY / HTTP_PROXY", () => {
|
||||||
|
const PROXY_VARS = [
|
||||||
|
"HTTPS_PROXY",
|
||||||
|
"https_proxy",
|
||||||
|
"HTTP_PROXY",
|
||||||
|
"http_proxy",
|
||||||
|
"NO_PROXY",
|
||||||
|
"no_proxy",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function clearProxyEnv() {
|
||||||
|
for (const name of PROXY_VARS) delete process.env[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearProxyEnv();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("readProxyEnv returns undefined with no proxy vars and the first set value otherwise", () => {
|
||||||
|
clearProxyEnv();
|
||||||
|
expect(readProxyEnv()).toBeUndefined();
|
||||||
|
process.env.HTTP_PROXY = "http://proxy.example:3128";
|
||||||
|
expect(readProxyEnv()).toBe("http://proxy.example:3128");
|
||||||
|
process.env.HTTPS_PROXY = "http://secure-proxy.example:3128";
|
||||||
|
// HTTPS_PROXY is checked before HTTP_PROXY
|
||||||
|
expect(readProxyEnv()).toBe("http://secure-proxy.example:3128");
|
||||||
|
clearProxyEnv();
|
||||||
|
process.env.https_proxy = " ";
|
||||||
|
expect(readProxyEnv()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("withProxyDispatcher returns the original fetch unchanged when no proxy is configured", () => {
|
||||||
|
clearProxyEnv();
|
||||||
|
const base = vi.fn();
|
||||||
|
expect(withProxyDispatcher(base as unknown as typeof fetch)).toBe(base);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("withProxyDispatcher injects an EnvHttpProxyAgent dispatcher into RequestInit", async () => {
|
||||||
|
clearProxyEnv();
|
||||||
|
process.env.HTTPS_PROXY = "http://proxy.example:3128";
|
||||||
|
const calls: Array<{
|
||||||
|
input: Parameters<typeof fetch>[0];
|
||||||
|
init: RequestInit | undefined;
|
||||||
|
}> = [];
|
||||||
|
const base = vi.fn(
|
||||||
|
async (input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||||
|
calls.push({ input, init });
|
||||||
|
return new Response("ok");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const proxied = withProxyDispatcher(base as unknown as typeof fetch);
|
||||||
|
expect(proxied).not.toBe(base);
|
||||||
|
await proxied("https://example.com/mcp", { method: "POST" });
|
||||||
|
expect(calls).toHaveLength(1);
|
||||||
|
expect(calls[0].input).toBe("https://example.com/mcp");
|
||||||
|
expect(calls[0].init?.method).toBe("POST");
|
||||||
|
const dispatcher = (calls[0].init as { dispatcher?: object }).dispatcher;
|
||||||
|
expect(dispatcher).toBeDefined();
|
||||||
|
expect(dispatcher?.constructor.name).toBe("EnvHttpProxyAgent");
|
||||||
|
// Second call reuses the same dispatcher (lazy singleton).
|
||||||
|
await proxied("https://example.com/mcp");
|
||||||
|
const dispatcher2 = (calls[1].init as { dispatcher?: object }).dispatcher;
|
||||||
|
expect(dispatcher2).toBe(dispatcher);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("createTransportNode wraps the supplied fetch with the proxy dispatcher for streamable-http", async () => {
|
||||||
|
clearProxyEnv();
|
||||||
|
process.env.HTTPS_PROXY = "http://proxy.example:3128";
|
||||||
|
let seenDispatcher: object | undefined;
|
||||||
|
const fetchFn = vi.fn(
|
||||||
|
async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||||
|
seenDispatcher = (init as { dispatcher?: object } | undefined)
|
||||||
|
?.dispatcher;
|
||||||
|
// Reject after capturing so connect() fails fast without hitting a
|
||||||
|
// real proxy.
|
||||||
|
throw new Error("stop");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const result = createTransportNode(
|
||||||
|
{ type: "streamable-http", url: "https://example.com/mcp" },
|
||||||
|
{ fetchFn: fetchFn as unknown as typeof fetch },
|
||||||
|
);
|
||||||
|
const client = new Client({ name: "t", version: "1" });
|
||||||
|
await expect(client.connect(result.transport)).rejects.toThrow();
|
||||||
|
expect(fetchFn).toHaveBeenCalled();
|
||||||
|
expect(seenDispatcher?.constructor.name).toBe("EnvHttpProxyAgent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an actionable error when a proxy is configured but undici cannot be loaded", async () => {
|
||||||
|
clearProxyEnv();
|
||||||
|
process.env.HTTPS_PROXY = "http://proxy.example:3128";
|
||||||
|
vi.doMock("undici", () => {
|
||||||
|
throw new Error("Cannot find module 'undici'");
|
||||||
|
});
|
||||||
|
// Re-import after the mock so the dynamic import("undici") inside
|
||||||
|
// withProxyDispatcher is intercepted.
|
||||||
|
const { withProxyDispatcher: wrap } =
|
||||||
|
await import("@inspector/core/mcp/node/transport.js");
|
||||||
|
const proxied = wrap(globalThis.fetch);
|
||||||
|
await expect(proxied("https://example.com")).rejects.toThrow(
|
||||||
|
/HTTPS_PROXY.*undici.*not.*available/i,
|
||||||
|
);
|
||||||
|
vi.doUnmock("undici");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,68 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
import { createFetchTracker } from "../fetchTracking.js";
|
import { createFetchTracker } from "../fetchTracking.js";
|
||||||
|
import type { Dispatcher } from "undici";
|
||||||
|
|
||||||
|
/** Node's `RequestInit` plus the undici-specific `dispatcher` option. */
|
||||||
|
type NodeRequestInit = RequestInit & { dispatcher?: Dispatcher };
|
||||||
|
|
||||||
|
/** Standard proxy env vars, in the precedence undici's EnvHttpProxyAgent uses. */
|
||||||
|
const PROXY_ENV_VARS = [
|
||||||
|
"HTTPS_PROXY",
|
||||||
|
"https_proxy",
|
||||||
|
"HTTP_PROXY",
|
||||||
|
"http_proxy",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First proxy URL found in the environment, or `undefined` if none is set.
|
||||||
|
* Exported for tests and so callers can decide whether proxying is in effect.
|
||||||
|
*/
|
||||||
|
export function readProxyEnv(): string | undefined {
|
||||||
|
for (const name of PROXY_ENV_VARS) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (value && value.trim() !== "") return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a fetch function so outbound HTTP/HTTPS requests honor the standard
|
||||||
|
* `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` environment variables.
|
||||||
|
*
|
||||||
|
* Node's built-in `fetch` (the bundled undici) accepts a `dispatcher` option
|
||||||
|
* in `RequestInit`; undici's `EnvHttpProxyAgent` reads the proxy env vars and
|
||||||
|
* routes per-request, so a single dispatcher covers both schemes and respects
|
||||||
|
* `NO_PROXY`. The `undici` package is loaded lazily on first call so callers
|
||||||
|
* that never set a proxy env var (TUI, web backend) need no extra dependency.
|
||||||
|
* If the proxy is configured but `undici` is unavailable, the wrapper throws
|
||||||
|
* with an actionable message rather than silently ignoring the env var.
|
||||||
|
*/
|
||||||
|
export function withProxyDispatcher(baseFetch: typeof fetch): typeof fetch {
|
||||||
|
if (readProxyEnv() === undefined) return baseFetch;
|
||||||
|
|
||||||
|
let dispatcherPromise: Promise<Dispatcher> | undefined;
|
||||||
|
const getDispatcher = (): Promise<Dispatcher> => {
|
||||||
|
dispatcherPromise ??= import("undici").then(
|
||||||
|
({ EnvHttpProxyAgent }) => new EnvHttpProxyAgent(),
|
||||||
|
(cause: unknown) => {
|
||||||
|
throw new Error(
|
||||||
|
"HTTPS_PROXY / HTTP_PROXY is set but the `undici` package is not " +
|
||||||
|
"available. Install it (it ships with the CLI client) or unset the " +
|
||||||
|
"proxy env var.",
|
||||||
|
{ cause },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return dispatcherPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxiedFetch: typeof fetch = async (input, init) => {
|
||||||
|
const dispatcher = await getDispatcher();
|
||||||
|
return baseFetch(input, { ...init, dispatcher } as NodeRequestInit);
|
||||||
|
};
|
||||||
|
return proxiedFetch;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the wire `headers` record from `settings.headers`, dropping rows with
|
* Build the wire `headers` record from `settings.headers`, dropping rows with
|
||||||
|
|
@ -48,7 +110,7 @@ export function createTransportNode(
|
||||||
settings,
|
settings,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const baseFetch = optionsFetchFn ?? globalThis.fetch;
|
const baseFetch = withProxyDispatcher(optionsFetchFn ?? globalThis.fetch);
|
||||||
|
|
||||||
if (serverType === "stdio") {
|
if (serverType === "stdio") {
|
||||||
const stdioConfig = config as StdioServerConfig;
|
const stdioConfig = config as StdioServerConfig;
|
||||||
|
|
@ -78,6 +140,8 @@ export function createTransportNode(
|
||||||
const sseConfig = config as SseServerConfig;
|
const sseConfig = config as SseServerConfig;
|
||||||
const url = new URL(sseConfig.url);
|
const url = new URL(sseConfig.url);
|
||||||
|
|
||||||
|
// A caller-supplied eventSourceInit.fetch wins as-is (explicit fetch is not
|
||||||
|
// re-wrapped for proxying); the default path uses the proxy-aware baseFetch.
|
||||||
const sseFetch =
|
const sseFetch =
|
||||||
(sseConfig.eventSourceInit?.fetch as typeof fetch) || baseFetch;
|
(sseConfig.eventSourceInit?.fetch as typeof fetch) || baseFetch;
|
||||||
const trackedFetch = onFetchRequest
|
const trackedFetch = onFetchRequest
|
||||||
|
|
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -26,6 +26,7 @@
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
"undici": "^8.5.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"yaml": "^2.9.0",
|
"yaml": "^2.9.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
|
|
@ -35,7 +36,7 @@
|
||||||
"mcp-inspector": "clients/launcher/build/index.js"
|
"mcp-inspector": "clients/launcher/build/index.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.7.5"
|
"node": ">=22.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alcalzone/ansi-tokenize": {
|
"node_modules/@alcalzone/ansi-tokenize": {
|
||||||
|
|
@ -3012,6 +3013,15 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "8.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz",
|
||||||
|
"integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"pino": "^9.14.0",
|
"pino": "^9.14.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
"undici": "^8.5.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"yaml": "^2.9.0",
|
"yaml": "^2.9.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
|
|
@ -81,6 +82,6 @@
|
||||||
"ink-select-input": "^6.2.0"
|
"ink-select-input": "^6.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.7.5"
|
"node": ">=22.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ Root scripts `inspector`, `web`, and `web:dev` are thin wrappers around the laun
|
||||||
|
|
||||||
**Production web caveat:** launcher `prebuild` chains `build:runner` for web, not a full `vite build`. Running `mcp-inspector --web` (without `--dev`) requires `clients/web/dist/` from a prior `cd clients/web && npm run build`. CI builds CLI, TUI, and launcher before the launcher smoke step but does not currently validate prod web startup end-to-end.
|
**Production web caveat:** launcher `prebuild` chains `build:runner` for web, not a full `vite build`. Running `mcp-inspector --web` (without `--dev`) requires `clients/web/dist/` from a prior `cd clients/web && npm run build`. CI builds CLI, TUI, and launcher before the launcher smoke step but does not currently validate prod web startup end-to-end.
|
||||||
|
|
||||||
**Build targets:** client `tsup` bundles target `node22`, aligned with root `engines.node` (`>=22.7.5`).
|
**Build targets:** client `tsup` bundles target `node22`, aligned with root `engines.node` (`>=22.19.0`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue