feat: allow complex JSON values in request _meta
Closes #1910 `_meta` was typed `Record<string, string>` end to end in the Inspector — on disk as a `{ key, value }[]` pair array, in the Server Settings form as key/value rows, and in every request verb's per-call metadata parameter. Nothing in the MCP spec restricts a `_meta` value to a string, and the SDK models the field as a `z.looseObject`, so the restriction was ours alone. Widen the whole path to `RequestMetadata` (`Record<string, JsonValue>`): - Per-server default metadata is a JSON **object** on disk and in memory. The pre-#1910 pair array is still read (`normalizeStoredMetadata`) so an existing `mcp.json` keeps working; it is never written back, so a save migrates the entry. - The Server Settings → Request Metadata section is a JSON editor (`JsonObjectInput`) instead of key/value rows — rows cannot express an object value. It keeps the typed text as the display source and emits only parsed JSON, the same draft split `SchemaJsonField` uses, so the box stays typeable; invalid text is flagged inline and not applied. - The CLI stops flattening `--metadata` / `--tool-metadata` through `metaValueToString`. Those values were already JSON-parsed, so `trace={"id":"abc"}` used to reach the wire as the *string* `{"id":"abc"}` and now reaches it as an object. - `servers/show` redaction replaces a sensitive key's whole value rather than a string, so a structured value cannot leak through its members. - `progressTokenOf` narrows the reserved `progressToken` key back to `string | number` before it reaches the SDK; any other JSON still travels inside `_meta` but wires up no progress callback. 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
6829b45f60
commit
b33532c024
20 changed files with 1004 additions and 495 deletions
|
|
@ -76,7 +76,7 @@ describe("runner client auth options", () => {
|
|||
taskTtl: 60000,
|
||||
maxFetchRequests: 10,
|
||||
autoRefreshOnListChanged: false,
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
headers: [],
|
||||
env: [],
|
||||
roots: [],
|
||||
|
|
@ -99,7 +99,7 @@ describe("runner client auth options", () => {
|
|||
taskTtl: 60000,
|
||||
maxFetchRequests: 10,
|
||||
autoRefreshOnListChanged: false,
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
headers: [],
|
||||
env: [],
|
||||
roots: [],
|
||||
|
|
@ -116,7 +116,7 @@ describe("runner client auth options", () => {
|
|||
taskTtl: 60000,
|
||||
maxFetchRequests: 10,
|
||||
autoRefreshOnListChanged: false,
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
headers: [],
|
||||
env: [],
|
||||
roots: [],
|
||||
|
|
@ -135,7 +135,7 @@ describe("runner client auth options", () => {
|
|||
taskTtl: 60000,
|
||||
maxFetchRequests: 10,
|
||||
autoRefreshOnListChanged: false,
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
headers: [],
|
||||
env: [],
|
||||
roots: [],
|
||||
|
|
@ -156,7 +156,7 @@ describe("runner client auth options", () => {
|
|||
taskTtl: 60000,
|
||||
maxFetchRequests: 10,
|
||||
autoRefreshOnListChanged: false,
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
headers: [],
|
||||
env: [],
|
||||
roots: [],
|
||||
|
|
@ -187,7 +187,7 @@ describe("runner client auth options", () => {
|
|||
taskTtl: 60000,
|
||||
maxFetchRequests: 10,
|
||||
autoRefreshOnListChanged: false,
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
headers: [],
|
||||
env: [],
|
||||
roots: [],
|
||||
|
|
|
|||
|
|
@ -854,7 +854,7 @@ describe("InspectorClient peer-handler timing (#1797)", () => {
|
|||
serverSettings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ describe("headersToServerSettings", () => {
|
|||
expect(settings?.headers).toEqual([
|
||||
{ key: "Authorization", value: "Bearer t" },
|
||||
]);
|
||||
expect(settings?.metadata).toEqual([]);
|
||||
expect(settings?.metadata).toEqual({});
|
||||
expect(settings?.roots).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ describe("RemoteClientTransport", () => {
|
|||
const settings = {
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
mcpConfigToServerEntries,
|
||||
mergeSecretsIntoStored,
|
||||
normalizeServerType,
|
||||
normalizeStoredMetadata,
|
||||
oauthAuthorizationParamsFromSettings,
|
||||
oauthEndpointOverridesFromSettings,
|
||||
serverEntriesToMcpConfig,
|
||||
|
|
@ -240,7 +241,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
});
|
||||
|
||||
it("round-trips a populated set of Inspector-extension fields (post-#1358 flat shape)", () => {
|
||||
// Disk shape: top-level `headers` (Record), `metadata` (pair-array),
|
||||
// Disk shape: top-level `headers` (Record), `metadata` (JSON object),
|
||||
// numeric timeouts, nested `oauth`. Round-trip must preserve the on-
|
||||
// disk shape byte-equivalent so a hand-edited file is stable.
|
||||
const original: MCPConfig = {
|
||||
|
|
@ -249,7 +250,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
type: "streamable-http",
|
||||
url: "https://x.test/mcp",
|
||||
headers: { Authorization: "Bearer xyz" },
|
||||
metadata: [{ key: "tenant", value: "acme" }],
|
||||
metadata: { tenant: "acme", limits: { rps: 10 } },
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
oauth: {
|
||||
|
|
@ -581,7 +582,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
// Non-stdio server → empty env mirror in memory (for the form)
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
// Absent taskTtl on disk → product default in memory (for the form)
|
||||
|
|
@ -663,7 +664,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
{ key: " ", value: "whitespace" },
|
||||
],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -690,7 +691,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -719,7 +720,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -758,7 +759,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -785,7 +786,7 @@ describe("serverEntriesToMcpConfig", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -1163,7 +1164,7 @@ describe("enterpriseManaged oauth settings", () => {
|
|||
const stored = inspectorSettingsToStoredFields({
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
@ -1180,7 +1181,7 @@ describe("enterpriseManaged oauth settings", () => {
|
|||
const stored = inspectorSettingsToStoredFields({
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
@ -1196,7 +1197,7 @@ describe("oauthOnInsufficientScope (SEP-2350)", () => {
|
|||
const baseSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
@ -1374,6 +1375,109 @@ describe("oauthOnInsufficientScope (SEP-2350)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("normalizeStoredMetadata (#1910)", () => {
|
||||
let warn: ReturnType<typeof vi.spyOn>;
|
||||
beforeEach(() => {
|
||||
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
});
|
||||
afterEach(() => {
|
||||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it("passes an object through, values of any JSON type intact", () => {
|
||||
const meta = {
|
||||
s: "a",
|
||||
n: 1,
|
||||
b: false,
|
||||
z: null,
|
||||
arr: [1, { k: 2 }],
|
||||
o: { deep: true },
|
||||
};
|
||||
expect(normalizeStoredMetadata(meta)).toEqual(meta);
|
||||
});
|
||||
|
||||
it("copies rather than aliasing the stored object", () => {
|
||||
// The settings object is edited in place by the form; sharing the
|
||||
// reference with the parsed catalog would mutate it behind the caller.
|
||||
const meta = { a: 1 };
|
||||
const out = normalizeStoredMetadata(meta);
|
||||
out.b = 2;
|
||||
expect(meta).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it("reads the pre-#1910 pair array so an existing mcp.json keeps working", () => {
|
||||
expect(
|
||||
normalizeStoredMetadata([
|
||||
{ key: "tenant", value: "acme" },
|
||||
{ key: "n", value: 3 },
|
||||
]),
|
||||
).toEqual({ tenant: "acme", n: 3 });
|
||||
});
|
||||
|
||||
it("skips a blank-key legacy pair", () => {
|
||||
expect(
|
||||
normalizeStoredMetadata([
|
||||
{ key: "", value: "orphan" },
|
||||
{ key: " ", value: "orphan" },
|
||||
{ key: "kept", value: "yes" },
|
||||
]),
|
||||
).toEqual({ kept: "yes" });
|
||||
});
|
||||
|
||||
it("reads a legacy pair with no value as an empty string", () => {
|
||||
expect(normalizeStoredMetadata([{ key: "k" }])).toEqual({ k: "" });
|
||||
});
|
||||
|
||||
it("drops a malformed legacy entry with a warning, keeping its siblings", () => {
|
||||
expect(
|
||||
normalizeStoredMetadata([
|
||||
"nope",
|
||||
null,
|
||||
{ value: "no key" },
|
||||
{ key: "kept", value: 1 },
|
||||
]),
|
||||
).toEqual({ kept: 1 });
|
||||
expect(warn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["undefined", undefined],
|
||||
["null", null],
|
||||
])("reads %s as the empty object without warning", (_label, input) => {
|
||||
expect(normalizeStoredMetadata(input)).toEqual({});
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a string", "oops"],
|
||||
["a number", 7],
|
||||
["a boolean", true],
|
||||
])("drops %s with a warning", (_label, input) => {
|
||||
expect(normalizeStoredMetadata(input)).toEqual({});
|
||||
expect(warn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("migrates a legacy file to the object shape on the next write", () => {
|
||||
// Read side accepts the pair array; write side emits only the object, so
|
||||
// one round-trip is the migration.
|
||||
const entries = mcpConfigToServerEntries({
|
||||
mcpServers: {
|
||||
legacy: {
|
||||
type: "streamable-http",
|
||||
url: "https://x.test/mcp",
|
||||
metadata: [{ key: "tenant", value: "acme" }] as unknown as Record<
|
||||
string,
|
||||
never
|
||||
>,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(entries[0]?.settings?.metadata).toEqual({ tenant: "acme" });
|
||||
const round = serverEntriesToMcpConfig(entries);
|
||||
expect(round.mcpServers.legacy?.metadata).toEqual({ tenant: "acme" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("envRecordToPairs / envPairsToRecord", () => {
|
||||
it("envRecordToPairs preserves key insertion order", () => {
|
||||
expect(envRecordToPairs({ B: "2", A: "1" })).toEqual([
|
||||
|
|
@ -1469,7 +1573,7 @@ describe("stdio env / cwd mirroring", () => {
|
|||
headers: [],
|
||||
env: [{ key: "API_KEY", value: "secret" }],
|
||||
cwd: "/srv/app",
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function prompt(name: string): Prompt {
|
|||
const AUTO_REFRESH_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function template(name: string): ResourceTemplate {
|
|||
const AUTO_REFRESH_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function resource(uri: string): Resource {
|
|||
const AUTO_REFRESH_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ function tool(name: string): Tool {
|
|||
const AUTO_REFRESH_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function prompt(name: string): Prompt {
|
|||
const PAGINATED_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function resource(uri: string): Resource {
|
|||
const PAGINATED_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function tool(name: string): Tool {
|
|||
const PAGINATED_SETTINGS: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ describe("useServers", () => {
|
|||
await result.current.updateServerSettings("alpha", {
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
env: [],
|
||||
metadata: [{ key: "trace", value: "abc" }],
|
||||
metadata: { trace: "abc" },
|
||||
connectionTimeout: 5000,
|
||||
requestTimeout: 30000,
|
||||
taskTtl: 30000,
|
||||
|
|
@ -425,7 +425,7 @@ describe("useServers", () => {
|
|||
expect(result.current.servers[0]?.settings).toEqual({
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
env: [],
|
||||
metadata: [{ key: "trace", value: "abc" }],
|
||||
metadata: { trace: "abc" },
|
||||
connectionTimeout: 5000,
|
||||
requestTimeout: 30000,
|
||||
taskTtl: 30000,
|
||||
|
|
@ -444,7 +444,7 @@ describe("useServers", () => {
|
|||
expect(stored.url).toBe("https://x.test/mcp");
|
||||
expect(stored).not.toHaveProperty("settings");
|
||||
expect(stored.headers).toEqual({ "X-Tenant": "acme" });
|
||||
expect(stored.metadata).toEqual([{ key: "trace", value: "abc" }]);
|
||||
expect(stored.metadata).toEqual({ trace: "abc" });
|
||||
expect(stored.connectionTimeout).toBe(5000);
|
||||
expect(stored.requestTimeout).toBe(30000);
|
||||
expect(stored.taskTtl).toBe(30000);
|
||||
|
|
@ -461,7 +461,7 @@ describe("useServers", () => {
|
|||
await result.current.updateServerSettings("nonexistent", {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -511,7 +511,7 @@ describe("useServers", () => {
|
|||
expect(result.current.servers[0]?.settings).toEqual({
|
||||
headers: [{ key: "X-Keep", value: "yes" }],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
// Absent taskTtl on disk reads back as the product default for the form.
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ describe("InspectorClient coverage backfill", () => {
|
|||
serverSettings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [{ key: "x-test", value: "1" }],
|
||||
metadata: { "x-test": "1" },
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -655,7 +655,7 @@ describe("InspectorClient coverage backfill", () => {
|
|||
serverSettings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 1,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
|
|||
|
|
@ -506,7 +506,7 @@ describe("modern-era negotiation (2026-07-28)", () => {
|
|||
versionNegotiation: eraToVersionNegotiation("modern"),
|
||||
serverSettings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
env: [],
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
|
|
@ -535,7 +535,7 @@ describe("modern-era negotiation (2026-07-28)", () => {
|
|||
versionNegotiation: eraToVersionNegotiation("modern"),
|
||||
serverSettings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
env: [],
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
|
|
@ -586,7 +586,7 @@ describe("modern-era negotiation (2026-07-28)", () => {
|
|||
versionNegotiation: eraToVersionNegotiation("modern"),
|
||||
serverSettings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
env: [],
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import type {
|
|||
MessageEntry,
|
||||
ConnectionStatus,
|
||||
FetchRequestEntryBase,
|
||||
RequestMetadata,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import type { JsonValue } from "@inspector/core/json/jsonUtils.js";
|
||||
import type {
|
||||
|
|
@ -164,7 +165,7 @@ function settleInFlight(call: Promise<unknown>): InFlightCall {
|
|||
/** Get all resources from the client via listResources() (paginates if needed). */
|
||||
async function getAllResources(
|
||||
client: InspectorClient,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<Resource[]> {
|
||||
const collected: Resource[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
|
@ -180,7 +181,7 @@ async function getAllResources(
|
|||
/** Get all resource templates via listResourceTemplates() (paginates if needed). */
|
||||
async function getAllResourceTemplates(
|
||||
client: InspectorClient,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<ResourceTemplate[]> {
|
||||
const collected: ResourceTemplate[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
|
@ -196,7 +197,7 @@ async function getAllResourceTemplates(
|
|||
/** Get all prompts via listPrompts() (paginates if needed). */
|
||||
async function getAllPrompts(
|
||||
client: InspectorClient,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<Prompt[]> {
|
||||
const collected: Prompt[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
|
@ -386,7 +387,7 @@ describe("InspectorClient", () => {
|
|||
serverSettings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 50,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -756,7 +757,7 @@ describe("InspectorClient", () => {
|
|||
const initial = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -1372,6 +1373,45 @@ describe("InspectorClient", () => {
|
|||
messageLogState.destroy();
|
||||
});
|
||||
|
||||
it("sends a structured default _meta value as JSON, not as a string (#1910)", async () => {
|
||||
// The whole point of #1910: a `_meta` value may be an object, an array,
|
||||
// a number, a boolean or `null`. The Inspector used to type this map as
|
||||
// `Record<string, string>`, so a nested payload could only be sent by
|
||||
// stringifying it — which is a different wire message than the one the
|
||||
// user configured.
|
||||
const structured = {
|
||||
trace: { id: "abc123", sampled: true, hops: [1, 2] },
|
||||
retries: 3,
|
||||
beta: false,
|
||||
unset: null,
|
||||
};
|
||||
client = new InspectorClient(
|
||||
{
|
||||
type: "stdio",
|
||||
command: serverCommand.command,
|
||||
args: serverCommand.args,
|
||||
},
|
||||
{
|
||||
environment: { transport: createTransportNode },
|
||||
defaultMetadata: structured,
|
||||
},
|
||||
);
|
||||
const messageLogState = new MessageLogState(client);
|
||||
await client.connect();
|
||||
await client.listTools();
|
||||
|
||||
const listToolsReq = messageLogState
|
||||
.getMessages()
|
||||
.find(
|
||||
(m) =>
|
||||
m.direction === "request" &&
|
||||
(m.message as { method?: string }).method === "tools/list",
|
||||
);
|
||||
expect(listToolsReq).toBeDefined();
|
||||
expect(metaOf(listToolsReq!)).toMatchObject(structured);
|
||||
messageLogState.destroy();
|
||||
});
|
||||
|
||||
it("call-time metadata overrides defaultMetadata on key collision", async () => {
|
||||
client = new InspectorClient(
|
||||
{
|
||||
|
|
@ -4241,7 +4281,7 @@ describe("InspectorClient", () => {
|
|||
timestamp: Date;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}> = [];
|
||||
|
||||
client!.addEventListener(
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ describe("Transport", () => {
|
|||
{ key: "", value: "ignored-empty-key" },
|
||||
],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -267,7 +267,7 @@ describe("Transport", () => {
|
|||
const baseSettings: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -305,7 +305,7 @@ describe("Transport", () => {
|
|||
const settings: InspectorServerSettings = {
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
@ -350,7 +350,7 @@ describe("Transport", () => {
|
|||
const settings: InspectorServerSettings = {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ describe("server.ts supplemental coverage", () => {
|
|||
|
||||
const base = {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
};
|
||||
|
|
@ -891,7 +891,7 @@ describe("server.ts supplemental coverage", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientSecret: "shh",
|
||||
|
|
@ -1039,7 +1039,7 @@ describe("server.ts supplemental coverage", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientSecret: "new-secret",
|
||||
|
|
|
|||
|
|
@ -425,10 +425,11 @@ describe("/api/servers routes", () => {
|
|||
// Wire envelope unchanged from #1353: pair-array headers, flat
|
||||
// oauth* fields. Backend splats these into the flat disk shape:
|
||||
// object headers, nested oauth, plus the inspector-only fields
|
||||
// at the top level.
|
||||
// at the top level. Metadata is the exception — it crosses as a
|
||||
// JSON object, since a `_meta` value may be any JSON (#1910).
|
||||
settings: {
|
||||
headers: [{ key: "Authorization", value: "Bearer xyz" }],
|
||||
metadata: [{ key: "tenant", value: "acme" }],
|
||||
metadata: { tenant: "acme", limits: { rps: 10 } },
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
oauthClientId: "client-abc",
|
||||
|
|
@ -442,7 +443,7 @@ describe("/api/servers routes", () => {
|
|||
// Disk shape: flat, no `settings` wrapper, object headers, nested oauth.
|
||||
expect(stored).not.toHaveProperty("settings");
|
||||
expect(stored.headers).toEqual({ Authorization: "Bearer xyz" });
|
||||
expect(stored.metadata).toEqual([{ key: "tenant", value: "acme" }]);
|
||||
expect(stored.metadata).toEqual({ tenant: "acme", limits: { rps: 10 } });
|
||||
expect(stored.connectionTimeout).toBe(30000);
|
||||
expect(stored.requestTimeout).toBe(60000);
|
||||
expect(stored.oauth).toEqual({
|
||||
|
|
@ -462,7 +463,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthAuthorizationParams: [
|
||||
|
|
@ -488,7 +489,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthAuthorizationParams: { kc_idp_hint: "corp" },
|
||||
|
|
@ -509,7 +510,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthAuthorizationUrl: "https://staging.test/authorize",
|
||||
|
|
@ -534,7 +535,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthAuthorizationUrl: "",
|
||||
|
|
@ -557,7 +558,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthTokenUrl: 42,
|
||||
|
|
@ -583,7 +584,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 45000,
|
||||
},
|
||||
|
|
@ -617,7 +618,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
autoRefreshOnListChanged: true,
|
||||
|
|
@ -649,7 +650,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
autoRefreshOnListChanged: false,
|
||||
|
|
@ -671,7 +672,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "stdio", command: "node" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
autoRefreshOnListChanged: "yes",
|
||||
|
|
@ -696,7 +697,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
maxFetchRequests: 5000,
|
||||
|
|
@ -724,7 +725,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
maxFetchRequests: 0,
|
||||
|
|
@ -756,7 +757,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
maxFetchRequests: 1000,
|
||||
|
|
@ -778,7 +779,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "stdio", command: "node" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
maxFetchRequests: -5,
|
||||
|
|
@ -884,7 +885,7 @@ describe("/api/servers routes", () => {
|
|||
// headers should be an array of {key, value}; "oops" is a string.
|
||||
settings: {
|
||||
headers: "oops",
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
},
|
||||
|
|
@ -915,7 +916,7 @@ describe("/api/servers routes", () => {
|
|||
// write lock and apply only the settings patch.
|
||||
settings: {
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
},
|
||||
|
|
@ -957,7 +958,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientId: "",
|
||||
|
|
@ -983,7 +984,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "stdio", command: "node" },
|
||||
settings: {
|
||||
headers: [{ key: "X-A", value: "1" }],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
// Unknown stowaway — must not survive the validator.
|
||||
|
|
@ -1101,7 +1102,7 @@ describe("/api/servers routes", () => {
|
|||
url: "https://x.test/mcp",
|
||||
settings: {
|
||||
headers: [{ key: "X-Tenant", value: "acme" }],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 0,
|
||||
oauthClientId: "client-abc",
|
||||
|
|
@ -1207,7 +1208,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "stdio", command: "node" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
roots: [
|
||||
|
|
@ -1255,7 +1256,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "stdio", command: "node" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
roots: [{ uri: " " }],
|
||||
|
|
@ -1285,7 +1286,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "stdio", command: "node" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
// uri must be a string.
|
||||
|
|
@ -1336,7 +1337,7 @@ describe("/api/servers routes", () => {
|
|||
{ key: "Authorization", value: "Bearer the-token" },
|
||||
{ key: "X-Tenant", value: "acme" },
|
||||
],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
},
|
||||
|
|
@ -1368,7 +1369,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: [{ key: "API_KEY", value: "abc-123" }],
|
||||
|
|
@ -1416,7 +1417,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 5000,
|
||||
requestTimeout: 0,
|
||||
},
|
||||
|
|
@ -1484,7 +1485,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: [],
|
||||
|
|
@ -1516,7 +1517,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: [
|
||||
|
|
@ -1587,7 +1588,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: "nope",
|
||||
|
|
@ -1614,7 +1615,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: [],
|
||||
|
|
@ -1642,7 +1643,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: [],
|
||||
|
|
@ -1675,7 +1676,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
env: [{ key: "API_KEY", value: "abc" }],
|
||||
|
|
@ -1775,7 +1776,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientId: "cid",
|
||||
|
|
@ -1935,7 +1936,7 @@ describe("/api/servers routes", () => {
|
|||
body: JSON.stringify({
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientId: "cid",
|
||||
|
|
@ -2327,7 +2328,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientId: "cid",
|
||||
|
|
@ -2361,7 +2362,7 @@ describe("/api/servers routes", () => {
|
|||
config: { type: "streamable-http", url: "https://x.test/mcp" },
|
||||
settings: {
|
||||
headers: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
oauthClientId: "cid",
|
||||
|
|
@ -2658,7 +2659,7 @@ describe("a config-only PUT must not delete stored secrets (#2083)", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
taskTtl: 60000,
|
||||
|
|
@ -2726,7 +2727,7 @@ describe("a config-only PUT must not delete stored secrets (#2083)", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
taskTtl: 60000,
|
||||
|
|
@ -2879,7 +2880,7 @@ describe("plaintext migration against a session-scoped store (#1950)", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 45000,
|
||||
requestTimeout: 60000,
|
||||
taskTtl: 60000,
|
||||
|
|
@ -2927,7 +2928,7 @@ describe("plaintext migration against a session-scoped store (#1950)", () => {
|
|||
settings: {
|
||||
headers: [],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 30000,
|
||||
requestTimeout: 60000,
|
||||
taskTtl: 60000,
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ describe("Remote transport e2e", () => {
|
|||
{ key: "X-Trace", value: "abc123" },
|
||||
],
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: 0,
|
||||
|
|
|
|||
Loading…
Reference in a new issue