Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
Tool,
} from "@modelcontextprotocol/client";
import { InspectorClient } from "@inspector/core/mcp/index.js";
import { toRecord } from "@inspector/core/json/jsonUtils.js";
import { getServerType } from "@inspector/core/mcp/config.js";
import type {
InspectorClientEventMap,
Expand Down Expand Up @@ -244,9 +245,7 @@ function getAuthToken(): string | undefined {
// ignore — see note above
}
};
const fromGlobal = (window as unknown as Record<string, unknown>)[
INSPECTOR_API_TOKEN_GLOBAL
];
const fromGlobal = toRecord(window)[INSPECTOR_API_TOKEN_GLOBAL];
if (typeof fromGlobal === "string" && fromGlobal) {
persist(fromGlobal);
return fromGlobal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function createMockBridge(): AppBridge {
removeEventListener: () => {},
teardownResource: async () => ({}),
close: async () => {},
// Partial mock: implements only the `AppBridge` members the renderer
// exercises; the double cast bridges the deliberately-incomplete shape.
} as unknown as AppBridge;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ function createMockBridge(): AppBridge {
close: async () => {},
addEventListener: () => {},
removeEventListener: () => {},
// Partial mock: implements only the `AppBridge` members the screen
// exercises; the double cast bridges the deliberately-incomplete shape.
} as unknown as AppBridge;
}

Expand Down Expand Up @@ -228,6 +230,8 @@ export const EchoRunning: Story = {
// Simulate the view finishing initialization shortly after AppRenderer
// registers its `initialized` listener; sendToolInput then flushes.
setTimeout(() => onInitialized?.(), 20);
// Partial mock (only the members this story drives); the double cast
// bridges the deliberately-incomplete shape.
return bridge as unknown as AppBridge;
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const noopBridgeFactory: BridgeFactory = () =>
sendToolCancelled: async () => {},
teardownResource: async () => ({}),
close: async () => {},
// Partial mock: implements only the `AppBridge` members the view exercises;
// the double cast bridges the deliberately-incomplete shape.
}) as unknown as AppBridge;

// MCP App tools — `isAppTool` detects these via `_meta.ui.resourceUri`,
Expand Down
3 changes: 3 additions & 0 deletions clients/web/src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Object.defineProperty(globalThis, "fetch", {
Object.defineProperty(window, "matchMedia", {
configurable: true,
writable: true,
// Partial `matchMedia` mock: happy-dom doesn't implement it, and the members
// below are all the app touches. The stub omits the rest of `MediaQueryList`,
// so the double cast bridges the deliberately-incomplete shape.
value: (query: string): MediaQueryList =>
({
matches: /prefers-reduced-motion/.test(query),
Expand Down
4 changes: 4 additions & 0 deletions core/auth/ema/resourceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export async function discoverEmaResourceContext(
const resourceAsUrl = getAuthorizationServerUrl(serverUrl, resourceMetadata);
const resourceUrl = await selectResourceURL(
serverUrl,
// `selectResourceURL` only reads `clientMetadata.scope` off the provider, so
// we pass a minimal stub carrying just that. A partial literal has too little
// overlap with the full `OAuthClientProvider` interface for a single cast, so
// the double cast bridges the deliberately-incomplete shape here.
{
clientMetadata: {
scope: configuredScope ?? "",
Expand Down
16 changes: 16 additions & 0 deletions core/json/jsonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ export type JsonValue =

export type JsonObject = { [key: string]: JsonValue };

/**
* 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
*/
Expand Down
12 changes: 8 additions & 4 deletions core/mcp/inspectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1892,12 +1892,16 @@ export class InspectorClient extends InspectorClientEventTarget {
throw new Error("Client is not connected");
}
const id = `inspector-ext-${(this.rawWireRequestCounter += 1)}`;
const message = {
jsonrpc: "2.0" as const,
// `params` is an arbitrary caller-supplied record; the SDK types request
// params with a specific optional `_meta` shape it can't satisfy, so widen
// it with a single structural cast. Typing `message` as `JSONRPCRequest`
// (a `JSONRPCMessage` member) then needs no further cast.
const message: JSONRPCRequest = {
jsonrpc: "2.0",
id,
method,
params,
} as unknown as JSONRPCMessage;
params: params as JSONRPCRequest["params"],
};
const timeoutMs = this.requestTimeout ?? 30_000;
const raw = await new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
Expand Down
5 changes: 2 additions & 3 deletions core/mcp/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
StreamableHttpServerConfig,
} from "../types.js";
import { normalizeServerType } from "../serverList.js";
import { toRecord } from "../../json/jsonUtils.js";

/**
* Options object passed to resolveServerConfigs by runners (parsed from argv).
Expand Down Expand Up @@ -140,9 +141,7 @@ function loadMcpServersConfig(

const normalizedServers: Record<string, MCPServerConfig> = {};
for (const [name, raw] of Object.entries(config.mcpServers)) {
normalizedServers[name] = normalizeServerType(
raw as unknown as Record<string, unknown> & { type?: string },
);
normalizedServers[name] = normalizeServerType(toRecord(raw));
}
return { ...config, mcpServers: normalizedServers };
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion core/mcp/remote/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
storedFieldsToInspectorSettings,
stripInspectorFields,
} from "../../serverList.js";
import { toRecord } from "../../../json/jsonUtils.js";
import { resolveImportSource } from "../../import/resolveSource.js";
import { RemoteSession } from "./remote-session.js";
import { createRemoteAuthProvider } from "./tokenAuthProvider.js";
Expand Down Expand Up @@ -298,7 +299,7 @@ function forwardLogEvent(
logEvent: Partial<LogEvent>,
): void {
const levelLabel = (logEvent?.level?.label ?? "info").toLowerCase();
const method = (logger as unknown as Record<string, unknown>)[levelLabel];
const method = toRecord(logger)[levelLabel];
if (typeof method !== "function") return;

const bindings = Object.assign(
Expand Down
6 changes: 6 additions & 0 deletions core/mcp/remote/node/tokenAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export function createRemoteAuthProvider(
state() {
return "";
},
// This provider implements only the members the remote backend transport
// exercises (token get/save, client info, non-interactive redirect); the
// interactive-only members the SDK never calls on the server side are stubbed
// with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
// interface's `string | Promise<string>`), so it can't be assigned to the
// full `OAuthClientProvider` without the double cast.
} as unknown as OAuthClientProvider;

return {
Expand Down
33 changes: 16 additions & 17 deletions core/mcp/serverList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
SECRET_FIELD_OAUTH_CLIENT_SECRET,
envSecretField,
} from "../auth/secret-fields.js";
import { toRecord } from "../json/jsonUtils.js";

// The full set of valid `type` discriminator values, used to reject anything
// else read off disk so unknown strings can't propagate to narrowing sites.
Expand Down Expand Up @@ -427,16 +428,17 @@ export const INSPECTOR_FIELD_KEYS = new Set(
* new extension field doesn't silently leak through this slice — the
* `satisfies` constraint above forces the map update, which propagates
* here.
*
* Every Inspector-extension key is optional on `StoredMCPServer`, so deleting
* them off a clone leaves a value still typed as (a subtype of)
* `MCPServerConfig` — no cast needed.
*/
export function stripInspectorFields(stored: StoredMCPServer): MCPServerConfig {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(
stored as unknown as Record<string, unknown>,
)) {
if (INSPECTOR_FIELD_KEYS.has(k as keyof StoredInspectorFields)) continue;
out[k] = v;
const out = { ...stored };
for (const key of INSPECTOR_FIELD_KEYS) {
delete out[key];
}
return out as unknown as MCPServerConfig;
return out;
}

/**
Expand All @@ -454,16 +456,13 @@ export function mcpConfigToServerEntries(config: MCPConfig): ServerEntry[] {
// `InspectorServerSettings` only.
const inspectorFields: StoredInspectorFields = {};
const sdkOnly: Record<string, unknown> = {};
// Widen the typed config object to a generic record to iterate its keys.
// `StoredMCPServer` has no index signature, so TS requires the `unknown`
// step (`as Record<string, unknown>` alone is TS2352). This is a plain
// structural widening, not an SDK-shape workaround. (Pre-existing pattern,
// also at the `serverEntryToStored` / oauth-strip casts in this file.)
for (const [k, v] of Object.entries(
raw as unknown as Record<string, unknown>,
)) {
// Partition the entry's keys into Inspector-extension vs SDK-only. Both
// `raw` and `inspectorFields` are widened through `toRecord` (see its doc)
// so the generic key iteration/assignment needs no per-site cast.
const inspectorRecord = toRecord(inspectorFields);
for (const [k, v] of Object.entries(toRecord(raw))) {
if (INSPECTOR_FIELD_KEYS.has(k as keyof StoredInspectorFields)) {
(inspectorFields as Record<string, unknown>)[k] = v;
inspectorRecord[k] = v;
} else {
sdkOnly[k] = v;
}
Expand Down Expand Up @@ -589,7 +588,7 @@ export function extractSecretsFromStored(
if (Object.keys(restOauth).length > 0) {
stripped.oauth = restOauth;
} else {
delete (stripped as unknown as Record<string, unknown>).oauth;
delete stripped.oauth;
}
}

Expand Down
8 changes: 5 additions & 3 deletions core/mcp/toolOutputValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ export function validateToolOutput(
: `Tool "${tool.name}" declares an output schema but returned no structured content`;
}
try {
const validate = provider.getValidator(
tool.outputSchema as unknown as JsonSchemaType,
);
// `tool.outputSchema` is the SDK's own object-schema shape; `JsonSchemaType`
// is the validator provider's third-party JSON Schema interface. The two are
// structurally compatible but nominally unrelated, so a cast is required to
// bridge them here.
const validate = provider.getValidator(tool.outputSchema as JsonSchemaType);
const validation = validate(structured);
return validation.valid ? undefined : validation.errorMessage;
} catch {
Expand Down
6 changes: 6 additions & 0 deletions test-servers/src/modern-tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ export function wireModernTaskHandlers(
runtime: ModernTaskRuntime,
): void {
const lowLevel = mcpServer.server;
// Reach the private `_requestHandlers` map (`RawHandlerHost`) to register a
// raw `tools/call` handler that skips the SDK's result-schema validation, so a
// task-augmented `CreateTaskResult` gets through. Mirrors the client-side
// bypass in `core/mcp/inspectorClient.ts` and the sibling in
// `composable-test-server.ts`; both go away when the SDK models the tasks
// extension natively.
const registry = (lowLevel as unknown as RawHandlerHost)._requestHandlers;
const sdkToolsCall = registry.get("tools/call");

Expand Down
9 changes: 7 additions & 2 deletions test-servers/src/test-server-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2041,7 +2041,9 @@ async function runTaskExecution(params: RunTaskExecutionParams): Promise<void> {
? z.union([ElicitResultSchema, CreateTaskResultSchema])
: ElicitResultSchema) as typeof ElicitResultSchema,
);
const elicitWithTask = elicitResponse as unknown as {
// The union result may carry a `task` handle that `ElicitResult` doesn't
// model. That field is optional, so a single narrowing cast reaches it.
const elicitWithTask = elicitResponse as {
task?: { taskId: string };
};
if (receiverTaskTtl != null && elicitWithTask?.task) {
Expand Down Expand Up @@ -2091,7 +2093,10 @@ async function runTaskExecution(params: RunTaskExecutionParams): Promise<void> {
? z.union([CreateMessageResultSchema, CreateTaskResultSchema])
: CreateMessageResultSchema) as typeof CreateMessageResultSchema,
);
const samplingWithTask = samplingResponse as unknown as {
// The union result may carry a `task` handle that `CreateMessageResult`
// doesn't model. That field is optional, so a single narrowing cast
// reaches it.
const samplingWithTask = samplingResponse as {
task?: { taskId: string };
};
if (receiverTaskTtl != null && samplingWithTask?.task) {
Expand Down
Loading