fix(web): carry a landed stdio env/cwd onto the config it connects with
The Server Settings modal edits a stdio server's `env` and `cwd` as settings, while the transport reads them off the *config* — so #2091's tracker, which resolves settings through the last write known to have reached disk, left those two behind: `server.config` still came straight off a `servers` entry that stops advancing once a list read fails. A save that landed during such a window kept spawning the child process with the pre-save environment, while the modal (re-seeded from the tracker) showed the new value. Credentials live in `env`, so the symptom is the inspected server failing to authorize rather than a visibly lost edit. Adds the settings→config mapping to `core/mcp/serverList.ts` — the one place both sides of the wire can share — and routes the `/api/servers` PUT write-through and the web client's connect path through it, so the value persisted and the value spawned cannot be derived differently. Closes #2096 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
2b129087a3
commit
d9b5f4f8a7
1 changed files with 289 additions and 5 deletions
|
|
@ -12,12 +12,18 @@ import {
|
|||
mergeSecretsIntoStored,
|
||||
normalizeServerType,
|
||||
normalizeStoredMetadata,
|
||||
applyStdioSettingsToConfig,
|
||||
stdioConfigFieldsFromSettings,
|
||||
oauthAuthorizationParamsFromSettings,
|
||||
oauthEndpointOverridesFromSettings,
|
||||
serverEntriesToMcpConfig,
|
||||
serializeMcpConfig,
|
||||
storedFieldsToInspectorSettings,
|
||||
} from "@inspector/core/mcp/serverList.js";
|
||||
import type {
|
||||
InspectorServerSettings,
|
||||
MCPServerConfig,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import {
|
||||
SECRET_FIELD_OAUTH_CLIENT_SECRET,
|
||||
envSecretField,
|
||||
|
|
@ -1771,3 +1777,122 @@ describe("cleanAuthorizationParams", () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stdio env/cwd settings → config mapping (#2096)", () => {
|
||||
// `env` and `cwd` are edited as settings but read by the transport off the
|
||||
// config, so both sides of the wire have to derive one from the other. This
|
||||
// is the single mapping they share — the PUT write-through persists it, the
|
||||
// web client applies it when constructing an InspectorClient.
|
||||
const settings = (
|
||||
over: Partial<InspectorServerSettings> = {},
|
||||
): InspectorServerSettings => ({
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
maxFetchRequests: 1000,
|
||||
roots: [],
|
||||
...over,
|
||||
});
|
||||
|
||||
describe("stdioConfigFieldsFromSettings", () => {
|
||||
it("collapses the pair rows into a record and trims the cwd", () => {
|
||||
expect(
|
||||
stdioConfigFieldsFromSettings(
|
||||
settings({
|
||||
env: [
|
||||
{ key: "FOO", value: "after" },
|
||||
{ key: "BAR", value: "" },
|
||||
],
|
||||
cwd: " /tmp/after ",
|
||||
}),
|
||||
),
|
||||
).toEqual({ env: { FOO: "after", BAR: "" }, cwd: "/tmp/after" });
|
||||
});
|
||||
|
||||
it("reports an empty list and a blank cwd as unset, i.e. cleared", () => {
|
||||
// The modal deletes a value by emptying it, so "no rows" cannot mean
|
||||
// "leave it alone" — it has to remove the field.
|
||||
expect(
|
||||
stdioConfigFieldsFromSettings(settings({ env: [], cwd: " " })),
|
||||
).toEqual({ env: undefined, cwd: undefined });
|
||||
// A row the user started and left blank is dropped, so it does not
|
||||
// materialize an env consisting of one nameless variable.
|
||||
expect(
|
||||
stdioConfigFieldsFromSettings(
|
||||
settings({ env: [{ key: " ", value: "x" }] }),
|
||||
).env,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyStdioSettingsToConfig", () => {
|
||||
const stdio: MCPServerConfig = {
|
||||
type: "stdio",
|
||||
command: "node",
|
||||
args: ["server.js"],
|
||||
env: { FOO: "before" },
|
||||
cwd: "/tmp/before",
|
||||
};
|
||||
|
||||
it("overwrites env/cwd and leaves the rest of the config alone", () => {
|
||||
expect(
|
||||
applyStdioSettingsToConfig(
|
||||
stdio,
|
||||
settings({
|
||||
env: [{ key: "FOO", value: "after" }],
|
||||
cwd: "/tmp/after",
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
type: "stdio",
|
||||
command: "node",
|
||||
args: ["server.js"],
|
||||
env: { FOO: "after" },
|
||||
cwd: "/tmp/after",
|
||||
});
|
||||
});
|
||||
|
||||
it("removes the fields the settings cleared", () => {
|
||||
expect(applyStdioSettingsToConfig(stdio, settings())).toEqual({
|
||||
type: "stdio",
|
||||
command: "node",
|
||||
args: ["server.js"],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not mutate the config it was given", () => {
|
||||
const before = { ...stdio };
|
||||
applyStdioSettingsToConfig(stdio, settings());
|
||||
expect(stdio).toEqual(before);
|
||||
});
|
||||
|
||||
it("treats a typeless entry as stdio, matching the on-disk default", () => {
|
||||
// An `mcp.json` entry written without a `type` key is stdio (the Claude
|
||||
// Desktop convention), and the modal edits its env like any other.
|
||||
const applied = applyStdioSettingsToConfig(
|
||||
{ command: "node" } as MCPServerConfig,
|
||||
settings({ env: [{ key: "FOO", value: "after" }] }),
|
||||
);
|
||||
expect(applied).toEqual({ command: "node", env: { FOO: "after" } });
|
||||
});
|
||||
|
||||
it("returns a non-stdio config untouched", () => {
|
||||
// An HTTP server carries neither field, matching the modal's stdio-only
|
||||
// UI — applying an empty mirror to it must not invent one.
|
||||
const http: MCPServerConfig = {
|
||||
type: "streamable-http",
|
||||
url: "https://mcp.example.com/mcp",
|
||||
};
|
||||
expect(applyStdioSettingsToConfig(http, settings())).toBe(http);
|
||||
});
|
||||
|
||||
it("returns the config untouched when there are no settings", () => {
|
||||
// A server with no settings node has nothing to apply; treating that as
|
||||
// an empty mirror would clear an env the file does hold.
|
||||
expect(applyStdioSettingsToConfig(stdio, undefined)).toBe(stdio);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue