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
1 changed files with 1004 additions and 495 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue