inspector/core/json/jsonUtils.ts
cliffhall 616dd9c196 fix: exclude undefined from _meta values, and cover the JSON worker
- `JsonValue` admits `undefined`, so `RequestMetadata` accepted values that do
  not survive serialization: `JSON.stringify` drops an `undefined` object
  member and rewrites an `undefined` array element to `null`. The type
  contradicted the one promise this payload makes. Added `StrictJsonValue` /
  `StrictJsonObject` and used them for the metadata path.

  Deliberately not a change to the repo-wide `JsonValue`: for tool *arguments*
  `undefined` legitimately means "not supplied". The narrowing is sound
  wherever it lands here, since every one of these values comes from
  `JSON.parse`, which cannot produce `undefined`. The CLI's `parseKeyValuePair`
  needed the same tightening — caught by `clients/cli`'s typecheck, which the
  web `tsc -b` does not cover.

- Nothing asserted the JSON worker's annotations, and the wrapper's own "Not
  valid JSON" message comes from our parser rather than from Ace — so a broken
  worker URL would have left every test passing while the gutter diagnostics
  silently disappeared. Added a Storybook play function that waits for a real
  `session.getAnnotations()` error entry, and verified it fails when the worker
  module URL is pointed at a bad path.

- `aria-describedby` referenced only the error, so a supplied `description` was
  never announced and would have been dropped entirely once the draft went
  invalid. Both ids are referenced now, with the description surviving the
  error.

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>
2026-08-23 18:27:28 -04:00

133 lines
3.7 KiB
TypeScript

import type { Tool } from "@modelcontextprotocol/client";
/**
* JSON value type used across the inspector project
*/
export type JsonValue =
| string
| number
| boolean
| null
| undefined
| JsonValue[]
| { [key: string]: JsonValue };
export type JsonObject = { [key: string]: JsonValue };
/**
* A JSON value that survives serialization unchanged — {@link JsonValue}
* without `undefined`.
*
* `JsonValue` admits `undefined` because it is also used for *arguments*, where
* it usefully means "this field was not supplied". That meaning does not carry
* to a payload the Inspector promises to transmit verbatim: `JSON.stringify`
* drops an `undefined` object member entirely and rewrites an `undefined` array
* element to `null`, so a value the type accepted is not the value that arrives.
*
* Used for `_meta` (#1910), whose whole contract is that any JSON the user
* writes reaches the wire as written.
*/
export type StrictJsonValue =
| string
| number
| boolean
| null
| StrictJsonValue[]
| { [key: string]: StrictJsonValue };
export type StrictJsonObject = { [key: string]: StrictJsonValue };
/**
* Widen a typed object to a generic string-keyed record so its keys can be
* iterated or read/written generically. Many of the project's config/SDK types
* (`StoredMCPServer`, `MCPServerConfig`, `pino.Logger`, DOM `Window`, …) have no
* index signature, so a direct `value as Record<string, unknown>` at a call
* site is a TS2352 error that would otherwise force an `as unknown as` double
* cast. Taking the argument as the general `object` type makes the single `as`
* legal — `Record<string, unknown>` is assignable to `object`, so the two types
* sufficiently overlap — letting this one audited spot own the widening while
* the double casts stay out of the call sites. Purely a structural view of the
* same object; no runtime effect.
*/
export function toRecord(value: object): Record<string, unknown> {
return value as Record<string, unknown>;
}
/**
* Simple schema type for parameter conversion
*/
type ParameterSchema = {
type?: string;
};
/**
* Convert a string parameter value to the appropriate JSON type based on schema
*/
export function convertParameterValue(
value: string,
schema: ParameterSchema,
): JsonValue {
if (!value) {
return value;
}
if (schema.type === "number" || schema.type === "integer") {
return Number(value);
}
if (schema.type === "boolean") {
return value.toLowerCase() === "true";
}
if (schema.type === "object" || schema.type === "array") {
try {
return JSON.parse(value) as JsonValue;
} catch {
return value;
}
}
return value;
}
/**
* Convert string parameters to JSON values based on tool schema
*/
export function convertToolParameters(
tool: Tool,
params: Record<string, string>,
): Record<string, JsonValue> {
const result: Record<string, JsonValue> = {};
const properties = tool.inputSchema?.properties || {};
for (const [key, value] of Object.entries(params)) {
const paramSchema = properties[key] as ParameterSchema | undefined;
if (paramSchema) {
result[key] = convertParameterValue(value, paramSchema);
} else {
result[key] = value;
}
}
return result;
}
/**
* Convert prompt arguments (JsonValue) to strings for prompt API
*/
export function convertPromptArguments(
args: Record<string, JsonValue>,
): Record<string, string> {
const stringArgs: Record<string, string> = {};
for (const [key, value] of Object.entries(args)) {
if (typeof value === "string") {
stringArgs[key] = value;
} else if (value === null || value === undefined) {
stringArgs[key] = String(value);
} else {
stringArgs[key] = JSON.stringify(value);
}
}
return stringArgs;
}