Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)
* refactor(web): update OAuth E2E tests to use new authentication method - Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration. - Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs. - Ensured that the tests continue to validate the expected authorization URL after the change. Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process. * Guided-auth removal, OAuth smoke testing, and TUI/CLI support * Added CLI OAuth runner flags tests * Added clarifying comment on port 0 binding * README notes on TUI oauth callback port, prettier fixes for CLI tests * CIMD validation uses inline like EMA IdP * Fixed TUI test failures from merge * test: bring 5 OAuth files over the 90 per-file coverage gate The guided-auth-removal work added/touched several files that fell below the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run the gate (it's local-only), so validate + test:integration passed while these gaps slipped through. Bring them all back over the line: - core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the provably-dead guards (empty host, out-of-range port, String(err) fallback, pathname "/" fallback) -- all rejected by new URL() upstream -- with justified v8 ignore comments instead of lowering the gate. - core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return. - core/react/useClientSettingsDraft.ts: test the functional-updater path, the stale-onChange-after-close prev===null guard, and the timer-fires-after-close value===null guard. - ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear. - clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration kinds, the expired IdP session, and the configured-scope/empty-scope branches of formatScopes. npm run coverage now passes (exit 0) across web, cli, tui, launcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: cliffhall <cliff@futurescale.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4a927d3978
commit
13dafe457b
7 changed files with 5702 additions and 4444 deletions
|
|
@ -100,6 +100,49 @@ Options that specify the MCP server (catalog/config file, ad-hoc command/URL, en
|
|||
| `--metadata <key=value>` | General metadata (key=value); applied to all methods. |
|
||||
| `--tool-metadata <key=value>` | Tool-specific metadata for `tools/call`. |
|
||||
|
||||
### CLI-specific (OAuth for HTTP servers)
|
||||
|
||||
The CLI **reuses** OAuth tokens from `~/.mcp-inspector/storage/oauth.json` (same file as the TUI). Complete first-time authorization in the **web** or **TUI** client, then run one-shot CLI commands against HTTP/SSE servers without signing in again.
|
||||
|
||||
The CLI does **not** start a local callback server or retry connect on 401. If tokens are missing or expired, connect fails; `ConsoleNavigation` may print an authorize URL to stdout, but the CLI cannot finish the redirect flow. Use the TUI for interactive runner OAuth until Phase 4 adds a CLI callback server.
|
||||
|
||||
**Shared with TUI** (config only, not interactive login):
|
||||
|
||||
- Per-server OAuth fields from `mcp.json` (static client, EMA resource credentials, scopes)
|
||||
- Install-level settings from **`~/.mcp-inspector/storage/client.json`** (or `--client-config` / `MCP_CLIENT_CONFIG_PATH`) — EMA IdP, CIMD
|
||||
- CLI flags `--client-id`, `--client-secret`, `--client-metadata-url` override `client.json` when set
|
||||
- Keychain-backed secrets in `mcp.json` are rehydrated on catalog load (same as TUI)
|
||||
|
||||
#### OAuth callback URL
|
||||
|
||||
| Surface | Default callback |
|
||||
| ------- | ---------------- |
|
||||
| **Web** | `http://localhost:6274/oauth/callback` |
|
||||
| **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) |
|
||||
|
||||
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
|
||||
|
||||
| Option | Env | Description |
|
||||
| ------ | --- | ----------- |
|
||||
| `--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-secret <secret>` | — | OAuth client secret; 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`). |
|
||||
|
||||
**Example** — list tools on an OAuth-protected server using stored tokens and CIMD from the command line:
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector --cli --catalog mcp.json --server my-http-server \
|
||||
--client-metadata-url https://example.com/.well-known/oauth/client-metadata.json \
|
||||
--method tools/list
|
||||
```
|
||||
|
||||
See [EMA / enterprise-managed auth](../../specification/v2_auth_ema.md) and [OAuth smoke testing](../../specification/v2_auth_smoke_testing.md) for configuration details and staging servers.
|
||||
|
||||
## Why use the CLI?
|
||||
|
||||
While the Web Client provides a rich visual interface, the CLI is designed for:
|
||||
|
|
|
|||
|
|
@ -88,3 +88,22 @@ export function createInvalidConfig(): string {
|
|||
export function deleteConfigFile(configPath: string): void {
|
||||
cleanupTempDir(path.dirname(configPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary install-level client.json for --client-config tests.
|
||||
*/
|
||||
export function createClientConfigFile(
|
||||
config: Record<string, unknown>,
|
||||
): string {
|
||||
const tempDir = createTempDir("mcp-inspector-client-config-");
|
||||
const configPath = path.join(tempDir, "client.json");
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
return configPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a client config file and its containing directory
|
||||
*/
|
||||
export function deleteClientConfigFile(configPath: string): void {
|
||||
cleanupTempDir(path.dirname(configPath));
|
||||
}
|
||||
|
|
|
|||
188
clients/cli/__tests__/oauth-runner.test.ts
Normal file
188
clients/cli/__tests__/oauth-runner.test.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import * as runner from "@inspector/core/client/runner.js";
|
||||
import {
|
||||
createTestServerHttp,
|
||||
createEchoTool,
|
||||
createTestServerInfo,
|
||||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
import { runCli } from "./helpers/cli-runner.js";
|
||||
import {
|
||||
expectCliFailure,
|
||||
expectCliSuccess,
|
||||
expectOutputContains,
|
||||
} from "./helpers/assertions.js";
|
||||
import {
|
||||
createClientConfigFile,
|
||||
deleteClientConfigFile,
|
||||
} from "./helpers/fixtures.js";
|
||||
|
||||
/**
|
||||
* Covers OAuth runner flag wiring in `src/cli.ts` (#1514): --client-config,
|
||||
* --client-id/--client-secret/--client-metadata-url, and --callback-url /
|
||||
* MCP_OAUTH_CALLBACK_URL. Shared auth logic is unit-tested in
|
||||
* clients/web/src/test/core/client/runner.test.ts; these assert the CLI
|
||||
* parses flags and passes them through on HTTP (OAuth-capable) connects.
|
||||
*/
|
||||
describe("CLI OAuth runner flags", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("connects over HTTP with OAuth CLI overrides and custom callback URL", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
const buildSpy = vi.spyOn(runner, "buildRunnerClientAuthOptions");
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--client-id",
|
||||
"test-client-id",
|
||||
"--client-secret",
|
||||
"test-client-secret",
|
||||
"--client-metadata-url",
|
||||
"https://example.com/oauth/client-metadata.json",
|
||||
"--callback-url",
|
||||
"http://127.0.0.1:9999/oauth/callback",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
expect(buildSpy).toHaveBeenCalled();
|
||||
expect(buildSpy.mock.calls[0]?.[2]).toEqual({
|
||||
clientId: "test-client-id",
|
||||
clientSecret: "test-client-secret",
|
||||
clientMetadataUrl: "https://example.com/oauth/client-metadata.json",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("loads install client.json from --client-config and prefers CLI CIMD override", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
const buildSpy = vi.spyOn(runner, "buildRunnerClientAuthOptions");
|
||||
const clientConfigPath = createClientConfigFile({
|
||||
cimd: {
|
||||
enabled: true,
|
||||
clientMetadataUrl: "https://example.com/from-client-json.json",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--client-config",
|
||||
clientConfigPath,
|
||||
"--client-metadata-url",
|
||||
"https://example.com/from-cli-flag.json",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
expect(buildSpy).toHaveBeenCalled();
|
||||
expect(buildSpy.mock.calls[0]?.[0]).toMatchObject({
|
||||
cimd: {
|
||||
enabled: true,
|
||||
clientMetadataUrl: "https://example.com/from-client-json.json",
|
||||
},
|
||||
});
|
||||
expect(buildSpy.mock.calls[0]?.[2]).toEqual({
|
||||
clientMetadataUrl: "https://example.com/from-cli-flag.json",
|
||||
});
|
||||
} finally {
|
||||
await server.stop();
|
||||
deleteClientConfigFile(clientConfigPath);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts port 0 callback URL for ephemeral listener binding", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--callback-url",
|
||||
"http://127.0.0.1:0/oauth/callback",
|
||||
]);
|
||||
|
||||
expectCliSuccess(result);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("uses MCP_OAUTH_CALLBACK_URL when --callback-url is absent", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli(
|
||||
[server.url, "--cli", "--method", "tools/list", "--transport", "http"],
|
||||
{
|
||||
env: {
|
||||
MCP_OAUTH_CALLBACK_URL:
|
||||
"http://127.0.0.1:8888/custom/oauth/callback",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expectCliSuccess(result);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an invalid OAuth callback URL before connecting", async () => {
|
||||
const server = createTestServerHttp({
|
||||
serverInfo: createTestServerInfo(),
|
||||
tools: [createEchoTool()],
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
const result = await runCli([
|
||||
server.url,
|
||||
"--cli",
|
||||
"--method",
|
||||
"tools/list",
|
||||
"--transport",
|
||||
"http",
|
||||
"--callback-url",
|
||||
"not-a-valid-url",
|
||||
]);
|
||||
|
||||
expectCliFailure(result);
|
||||
expectOutputContains(result, "Invalid OAuth callback URL");
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
220
clients/cli/package-lock.json
generated
220
clients/cli/package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@napi-rs/keyring": "^1.3.0",
|
||||
"ajv": "^8.17.1",
|
||||
"atomically": "^2.1.1",
|
||||
"commander": "^13.1.0",
|
||||
|
|
@ -907,6 +908,225 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz",
|
||||
"integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/keyring-darwin-arm64": "1.3.0",
|
||||
"@napi-rs/keyring-darwin-x64": "1.3.0",
|
||||
"@napi-rs/keyring-freebsd-x64": "1.3.0",
|
||||
"@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0",
|
||||
"@napi-rs/keyring-linux-arm64-gnu": "1.3.0",
|
||||
"@napi-rs/keyring-linux-arm64-musl": "1.3.0",
|
||||
"@napi-rs/keyring-linux-riscv64-gnu": "1.3.0",
|
||||
"@napi-rs/keyring-linux-x64-gnu": "1.3.0",
|
||||
"@napi-rs/keyring-linux-x64-musl": "1.3.0",
|
||||
"@napi-rs/keyring-win32-arm64-msvc": "1.3.0",
|
||||
"@napi-rs/keyring-win32-ia32-msvc": "1.3.0",
|
||||
"@napi-rs/keyring-win32-x64-msvc": "1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-darwin-arm64": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz",
|
||||
"integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-darwin-x64": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz",
|
||||
"integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-freebsd-x64": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz",
|
||||
"integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-linux-arm-gnueabihf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz",
|
||||
"integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-linux-arm64-gnu": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz",
|
||||
"integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-linux-arm64-musl": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz",
|
||||
"integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-linux-riscv64-gnu": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz",
|
||||
"integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-linux-x64-gnu": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz",
|
||||
"integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-linux-x64-musl": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz",
|
||||
"integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-win32-arm64-msvc": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz",
|
||||
"integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-win32-ia32-msvc": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz",
|
||||
"integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/keyring-win32-x64-msvc": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz",
|
||||
"integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@napi-rs/keyring": "^1.3.0",
|
||||
"ajv": "^8.17.1",
|
||||
"atomically": "^2.1.1",
|
||||
"commander": "^13.1.0",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { awaitableLog } from "./utils/awaitable-log.js";
|
|||
import type {
|
||||
InspectorServerSettings,
|
||||
MCPServerConfig,
|
||||
InspectorClientEnvironment,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import {
|
||||
|
|
@ -22,6 +23,24 @@ import {
|
|||
parseHeaderPair,
|
||||
} from "@inspector/core/mcp/node/index.js";
|
||||
import type { JsonValue } from "@inspector/core/mcp/index.js";
|
||||
import {
|
||||
ConsoleNavigation,
|
||||
MutableRedirectUrlProvider,
|
||||
} from "@inspector/core/auth/index.js";
|
||||
import { NodeOAuthStorage } from "@inspector/core/auth/node/index.js";
|
||||
import {
|
||||
DEFAULT_RUNNER_OAUTH_CALLBACK_URL,
|
||||
formatRunnerOAuthRedirectUrl,
|
||||
parseRunnerOAuthCallbackUrl,
|
||||
type RunnerOAuthCallbackConfig,
|
||||
} from "@inspector/core/auth/node/runner-oauth-callback.js";
|
||||
import type { ClientConfig } from "@inspector/core/client/types.js";
|
||||
import {
|
||||
buildRunnerClientAuthOptions,
|
||||
isOAuthCapableServerConfig,
|
||||
loadRunnerClientConfig,
|
||||
type RunnerClientConfigOverrides,
|
||||
} from "@inspector/core/client/runner.js";
|
||||
import {
|
||||
LoggingLevelSchema,
|
||||
type LoggingLevel,
|
||||
|
|
@ -47,6 +66,9 @@ async function callMethod(
|
|||
serverConfig: MCPServerConfig,
|
||||
serverSettings: InspectorServerSettings | undefined,
|
||||
args: MethodArgs & { method: string },
|
||||
clientConfig: ClientConfig,
|
||||
cliAuthOverrides?: RunnerClientConfigOverrides,
|
||||
callbackUrlConfig?: RunnerOAuthCallbackConfig,
|
||||
): Promise<void> {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const packageJsonPath = join(__dirname, "../package.json");
|
||||
|
|
@ -62,16 +84,36 @@ async function callMethod(
|
|||
const version = packageJson.version;
|
||||
const clientIdentity = { name, version };
|
||||
|
||||
const environment: InspectorClientEnvironment = {
|
||||
transport: createTransportNode,
|
||||
};
|
||||
const redirectUrlProvider = new MutableRedirectUrlProvider();
|
||||
if (isOAuthCapableServerConfig(serverConfig)) {
|
||||
redirectUrlProvider.redirectUrl = formatRunnerOAuthRedirectUrl(
|
||||
callbackUrlConfig!,
|
||||
);
|
||||
environment.oauth = {
|
||||
storage: new NodeOAuthStorage(),
|
||||
navigation: new ConsoleNavigation(),
|
||||
redirectUrlProvider,
|
||||
};
|
||||
}
|
||||
|
||||
const clientAuthOptions = buildRunnerClientAuthOptions(
|
||||
clientConfig,
|
||||
serverSettings,
|
||||
cliAuthOverrides,
|
||||
);
|
||||
|
||||
const inspectorClient = new InspectorClient(serverConfig, {
|
||||
environment: {
|
||||
transport: createTransportNode,
|
||||
},
|
||||
environment,
|
||||
clientIdentity,
|
||||
initialLoggingLevel: "debug",
|
||||
progress: false,
|
||||
sample: false,
|
||||
elicit: false,
|
||||
serverSettings,
|
||||
...clientAuthOptions,
|
||||
});
|
||||
|
||||
let managedToolsState: ManagedToolsState | null = null;
|
||||
|
|
@ -240,11 +282,16 @@ function parseKeyValuePair(
|
|||
return { ...previous, [key as string]: parsedValue };
|
||||
}
|
||||
|
||||
function parseArgs(argv?: string[]): {
|
||||
async function parseArgs(argv?: string[]): Promise<{
|
||||
serverConfig: MCPServerConfig;
|
||||
serverSettings: InspectorServerSettings | undefined;
|
||||
methodArgs: MethodArgs & { method: string };
|
||||
} {
|
||||
clientConfigPath?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
clientMetadataUrl?: string;
|
||||
callbackUrl?: string;
|
||||
}> {
|
||||
const program = new Command();
|
||||
// On a parse/usage ERROR (exitCode !== 0), throw the CommanderError instead
|
||||
// of letting commander call process.exit(). The binary entry (index.ts) still
|
||||
|
|
@ -368,6 +415,26 @@ function parseArgs(argv?: string[]): {
|
|||
"Tool-specific metadata as key=value pairs (for tools/call method only)",
|
||||
parseKeyValuePair,
|
||||
{},
|
||||
)
|
||||
.option(
|
||||
"--client-config <path>",
|
||||
"Install-level client config (default: ~/.mcp-inspector/storage/client.json, or MCP_CLIENT_CONFIG_PATH)",
|
||||
)
|
||||
.option(
|
||||
"--client-id <id>",
|
||||
"OAuth client ID (static client) for HTTP servers",
|
||||
)
|
||||
.option(
|
||||
"--client-secret <secret>",
|
||||
"OAuth client secret (for confidential clients)",
|
||||
)
|
||||
.option(
|
||||
"--client-metadata-url <url>",
|
||||
"OAuth Client ID Metadata Document URL (CIMD) for HTTP servers",
|
||||
)
|
||||
.option(
|
||||
"--callback-url <url>",
|
||||
`OAuth redirect/callback listener URL (default: ${DEFAULT_RUNNER_OAUTH_CALLBACK_URL}, or MCP_OAUTH_CALLBACK_URL)`,
|
||||
);
|
||||
|
||||
program.parse(preArgs);
|
||||
|
|
@ -390,6 +457,11 @@ function parseArgs(argv?: string[]): {
|
|||
transport?: "sse" | "http" | "stdio";
|
||||
serverUrl?: string;
|
||||
header?: Record<string, string>;
|
||||
clientConfig?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
clientMetadataUrl?: string;
|
||||
callbackUrl?: string;
|
||||
};
|
||||
|
||||
const serverOptions = {
|
||||
|
|
@ -410,7 +482,7 @@ function parseArgs(argv?: string[]): {
|
|||
// Shared with the TUI: resolves the catalog/config source (or ad-hoc target),
|
||||
// enforces the conflict matrix, and lifts disk headers/timeouts/OAuth into
|
||||
// per-server settings. `--server` selects one when the file has several.
|
||||
const entries = loadServerEntries(serverOptions);
|
||||
const entries = await loadServerEntries(serverOptions);
|
||||
const { config: serverConfig, settings: serverSettings } = selectServerEntry(
|
||||
entries,
|
||||
options.server,
|
||||
|
|
@ -452,12 +524,37 @@ function parseArgs(argv?: string[]): {
|
|||
serverConfig,
|
||||
serverSettings,
|
||||
methodArgs,
|
||||
clientConfigPath: options.clientConfig,
|
||||
clientId: options.clientId,
|
||||
clientSecret: options.clientSecret,
|
||||
clientMetadataUrl: options.clientMetadataUrl,
|
||||
callbackUrl: options.callbackUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCli(argv?: string[]): Promise<void> {
|
||||
const { serverConfig, serverSettings, methodArgs } = parseArgs(
|
||||
argv ?? process.argv,
|
||||
const {
|
||||
serverConfig,
|
||||
serverSettings,
|
||||
methodArgs,
|
||||
clientConfigPath,
|
||||
clientId,
|
||||
clientSecret,
|
||||
clientMetadataUrl,
|
||||
callbackUrl,
|
||||
} = await parseArgs(argv ?? process.argv);
|
||||
const clientConfig = await loadRunnerClientConfig({ clientConfigPath });
|
||||
const callbackUrlConfig = parseRunnerOAuthCallbackUrl(callbackUrl);
|
||||
await callMethod(
|
||||
serverConfig,
|
||||
serverSettings,
|
||||
methodArgs,
|
||||
clientConfig,
|
||||
{
|
||||
clientId,
|
||||
clientSecret,
|
||||
clientMetadataUrl,
|
||||
},
|
||||
callbackUrlConfig,
|
||||
);
|
||||
await callMethod(serverConfig, serverSettings, methodArgs);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export default defineConfig({
|
|||
platform: 'node',
|
||||
// Bundle core source; leave npm deps external.
|
||||
noExternal: [/^@inspector\/core/],
|
||||
external: ['@napi-rs/keyring', '@modelcontextprotocol/sdk', 'commander', 'pino', 'zustand'],
|
||||
esbuildOptions(options) {
|
||||
options.alias = {
|
||||
'@inspector/core': path.join(repoRoot, 'core'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue