Stands up the type-only prerequisites for the v2 UX component
interfaces refactor per specification/v2_ux_interfaces_plan.md.
Phase 0.1 — MCP SDK dependency:
- Add @modelcontextprotocol/sdk ^1.29.0 and zod ^4.3.6 to both the
root package.json and clients/web/package.json so component files
can import Tool / Prompt / Resource / Task / LoggingLevel / etc.
directly from the SDK.
Phase 0.2 — Inspector wrapper-types module:
- Create core/mcp/types.ts mirroring the type-only subset of v1.5's
core/mcp/types.ts: MCPServerConfig (discriminated union), ServerType,
MCPConfig, ConnectionStatus, StderrLogEntry, MessageEntry (Date
timestamps, not ISO strings), FetchRequestEntry(+Base), ServerState,
and the *Invocation types. Adds v2-only wrappers
InspectorResourceSubscription and InspectorServerJsonDraft.
- Add core/mcp/taskNotificationSchemas.ts for the Inspector-owned
notifications/tasks/list_changed signal (not in the SDK).
- Add core/mcp/elicitationCreateMessage.ts re-exporting ElicitRequest
and ElicitResult plus an InspectorPendingElicitation interface for
typing the pending-queue.
- Add core/json/jsonUtils.ts for the shared JsonValue / JsonObject
types used inside core/mcp/types.ts.
- Copy v1.5's clients/web/src/utils/jsonUtils.ts (JsonSchemaType) and
schemaUtils.ts (AJV-backed validators) verbatim so SchemaForm can
switch off its local JsonSchema type.
- Copy v1.5's clients/web/src/lib/types/customHeaders.ts verbatim for
ServerSettingsForm and the experimental panel.
- Add clients/web/src/types/navigation.ts with InspectorTab (UI-routing
concept, lives with the web client rather than in core/).
- Wire tsconfig.app.json + vite.config.ts with the @inspector/core/*
path alias so component files can import from core/ without
relative-path hell.
Phase 0.3 — Vet (likely) hedges in v2_ux_interfaces.md:
- Follow-up sweep on commit 2ef257da. Replaces the remaining
useMcpClient references with useInspectorClient, ties pending
elicitation / sampling callbacks to the in-InspectorClient queue
rather than a non-existent useElicitation / useSampling hook,
corrects Task references to the SDK type (not an InspectorTask
wrapper), corrects log hook wiring to useMessageLog, and strikes
all remaining v2-only hooks with explicit "no v1.5 analog"
annotations. grep "likely" specification/v2_ux_interfaces.md now
returns zero results.
Phase 0.4 — Local-redeclaration inventory:
- specification/v2_ux_interfaces_phase_0_4_inventory.md enumerates
every ad hoc local type (JsonSchema, LogLevel, TaskStatus,
PromptItem, ResourceItem, ToolListItemProps, RootEntry, KeyValuePair,
inline ConnectionStatus / transport unions) that will be replaced
in Phases 1–4, including the file it lives in, every site that
re-imports it, and the SDK / wrapper type that replaces it. Intended
for the Phase 0 PR description and as a running checklist during
the refactor.
Build state: npm run build / lint / format:check all pass in
clients/web.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
252 lines
5.7 KiB
TypeScript
252 lines
5.7 KiB
TypeScript
export type JsonValue =
|
|
| string
|
|
| number
|
|
| boolean
|
|
| null
|
|
| undefined
|
|
| JsonValue[]
|
|
| { [key: string]: JsonValue };
|
|
|
|
export type JsonSchemaConst = {
|
|
const: JsonValue;
|
|
title?: string;
|
|
description?: string;
|
|
};
|
|
|
|
export type JsonSchemaType = {
|
|
type?:
|
|
| "string"
|
|
| "number"
|
|
| "integer"
|
|
| "boolean"
|
|
| "array"
|
|
| "object"
|
|
| "null"
|
|
| (
|
|
| "string"
|
|
| "number"
|
|
| "integer"
|
|
| "boolean"
|
|
| "array"
|
|
| "object"
|
|
| "null"
|
|
)[];
|
|
title?: string;
|
|
description?: string;
|
|
required?: string[];
|
|
default?: JsonValue;
|
|
properties?: Record<string, JsonSchemaType>;
|
|
items?: JsonSchemaType;
|
|
// Array validation constraints
|
|
minItems?: number;
|
|
maxItems?: number;
|
|
minimum?: number;
|
|
maximum?: number;
|
|
minLength?: number;
|
|
maxLength?: number;
|
|
nullable?: boolean;
|
|
pattern?: string;
|
|
format?: string;
|
|
enum?: string[];
|
|
// Non-standard legacy support: titles for enum values
|
|
enumNames?: string[];
|
|
const?: JsonValue;
|
|
oneOf?: (JsonSchemaType | JsonSchemaConst)[];
|
|
anyOf?: (JsonSchemaType | JsonSchemaConst)[];
|
|
$ref?: string;
|
|
};
|
|
|
|
export type JsonObject = { [key: string]: JsonValue };
|
|
|
|
export type DataType =
|
|
| "string"
|
|
| "number"
|
|
| "bigint"
|
|
| "boolean"
|
|
| "symbol"
|
|
| "undefined"
|
|
| "object"
|
|
| "function"
|
|
| "array"
|
|
| "null";
|
|
|
|
/**
|
|
* Determines the specific data type of a JSON value
|
|
* @param value The JSON value to analyze
|
|
* @returns The specific data type including "array" and "null" as distinct types
|
|
*/
|
|
export function getDataType(value: JsonValue): DataType {
|
|
if (Array.isArray(value)) return "array";
|
|
if (value === null) return "null";
|
|
return typeof value;
|
|
}
|
|
|
|
/**
|
|
* Attempts to parse a string as JSON, only for objects and arrays
|
|
* @param str The string to parse
|
|
* @returns Object with success boolean and either parsed data or original string
|
|
*/
|
|
export function tryParseJson(str: string): {
|
|
success: boolean;
|
|
data: JsonValue;
|
|
} {
|
|
const trimmed = str?.trim();
|
|
if (
|
|
trimmed &&
|
|
!(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
|
|
!(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
|
) {
|
|
return { success: false, data: str };
|
|
}
|
|
try {
|
|
return { success: true, data: JSON.parse(str) };
|
|
} catch {
|
|
return { success: false, data: str };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a value at a specific path in a nested JSON structure
|
|
* @param obj The original JSON value
|
|
* @param path Array of keys/indices representing the path to the value
|
|
* @param value The new value to set
|
|
* @returns A new JSON value with the updated path
|
|
*/
|
|
export function updateValueAtPath(
|
|
obj: JsonValue,
|
|
path: string[],
|
|
value: JsonValue,
|
|
): JsonValue {
|
|
if (path.length === 0) return value;
|
|
|
|
if (obj === null || obj === undefined) {
|
|
obj = !isNaN(Number(path[0])) ? [] : {};
|
|
}
|
|
|
|
if (Array.isArray(obj)) {
|
|
return updateArray(obj, path, value);
|
|
} else if (typeof obj === "object" && obj !== null) {
|
|
return updateObject(obj as JsonObject, path, value);
|
|
} else {
|
|
console.error(
|
|
`Cannot update path ${path.join(".")} in non-object/array value:`,
|
|
obj,
|
|
);
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates an array at a specific path
|
|
*/
|
|
function updateArray(
|
|
array: JsonValue[],
|
|
path: string[],
|
|
value: JsonValue,
|
|
): JsonValue[] {
|
|
const [index, ...restPath] = path;
|
|
const arrayIndex = Number(index);
|
|
|
|
if (isNaN(arrayIndex)) {
|
|
console.error(`Invalid array index: ${index}`);
|
|
return array;
|
|
}
|
|
|
|
if (arrayIndex < 0) {
|
|
console.error(`Array index out of bounds: ${arrayIndex} < 0`);
|
|
return array;
|
|
}
|
|
|
|
let newArray: JsonValue[] = [];
|
|
for (let i = 0; i < array.length; i++) {
|
|
newArray[i] = i in array ? array[i] : null;
|
|
}
|
|
|
|
if (arrayIndex >= newArray.length) {
|
|
const extendedArray: JsonValue[] = new Array(arrayIndex).fill(null);
|
|
// Copy over the existing elements (now guaranteed to be dense)
|
|
for (let i = 0; i < newArray.length; i++) {
|
|
extendedArray[i] = newArray[i];
|
|
}
|
|
newArray = extendedArray;
|
|
}
|
|
|
|
if (restPath.length === 0) {
|
|
newArray[arrayIndex] = value;
|
|
} else {
|
|
newArray[arrayIndex] = updateValueAtPath(
|
|
newArray[arrayIndex],
|
|
restPath,
|
|
value,
|
|
);
|
|
}
|
|
return newArray;
|
|
}
|
|
|
|
/**
|
|
* Updates an object at a specific path
|
|
*/
|
|
function updateObject(
|
|
obj: JsonObject,
|
|
path: string[],
|
|
value: JsonValue,
|
|
): JsonObject {
|
|
const [key, ...restPath] = path;
|
|
|
|
// Validate object key
|
|
if (typeof key !== "string") {
|
|
console.error(`Invalid object key: ${key}`);
|
|
return obj;
|
|
}
|
|
|
|
const newObj = { ...obj };
|
|
|
|
if (restPath.length === 0) {
|
|
newObj[key] = value;
|
|
} else {
|
|
// Ensure key exists
|
|
if (!(key in newObj)) {
|
|
newObj[key] = {};
|
|
}
|
|
newObj[key] = updateValueAtPath(newObj[key], restPath, value);
|
|
}
|
|
return newObj;
|
|
}
|
|
|
|
/**
|
|
* Gets a value at a specific path in a nested JSON structure
|
|
* @param obj The JSON value to traverse
|
|
* @param path Array of keys/indices representing the path to the value
|
|
* @param defaultValue Value to return if path doesn't exist
|
|
* @returns The value at the path, or defaultValue if not found
|
|
*/
|
|
export function getValueAtPath(
|
|
obj: JsonValue,
|
|
path: string[],
|
|
defaultValue: JsonValue = null,
|
|
): JsonValue {
|
|
if (path.length === 0) return obj;
|
|
|
|
const [first, ...rest] = path;
|
|
|
|
if (obj === null || obj === undefined) {
|
|
return defaultValue;
|
|
}
|
|
|
|
if (Array.isArray(obj)) {
|
|
const index = Number(first);
|
|
if (isNaN(index) || index < 0 || index >= obj.length) {
|
|
return defaultValue;
|
|
}
|
|
return getValueAtPath(obj[index], rest, defaultValue);
|
|
}
|
|
|
|
if (typeof obj === "object" && obj !== null) {
|
|
if (!(first in obj)) {
|
|
return defaultValue;
|
|
}
|
|
return getValueAtPath((obj as JsonObject)[first], rest, defaultValue);
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|