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
12 changed files with 1004 additions and 495 deletions
|
|
@ -36,6 +36,7 @@ import type {
|
|||
ResourceSubscriptionStreamState,
|
||||
ToolCallInvocation,
|
||||
ExcludedTool,
|
||||
RequestMetadata,
|
||||
} from "../types.js";
|
||||
import { INACTIVE_SUBSCRIPTION_STREAM_STATE } from "../types.js";
|
||||
import type { MalformedListItem } from "../listSalvage.js";
|
||||
|
|
@ -145,25 +146,25 @@ export class FakeInspectorClient
|
|||
listAllTools = vi.fn(
|
||||
async (_options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}) => ({ tools: drainPages(this.toolPages, "tools") }),
|
||||
);
|
||||
listAllPrompts = vi.fn(
|
||||
async (_options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}) => ({ prompts: drainPages(this.promptPages, "prompts") }),
|
||||
);
|
||||
listAllResources = vi.fn(
|
||||
async (_options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}) => ({ resources: drainPages(this.resourcePages, "resources") }),
|
||||
);
|
||||
listAllResourceTemplates = vi.fn(
|
||||
async (_options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}) => ({
|
||||
resourceTemplates: drainPages(
|
||||
this.resourceTemplatePages,
|
||||
|
|
@ -224,7 +225,7 @@ export class FakeInspectorClient
|
|||
_argumentName: string,
|
||||
_argumentValue: string,
|
||||
_context?: Record<string, string>,
|
||||
_metadata?: Record<string, string>,
|
||||
_metadata?: RequestMetadata,
|
||||
): Promise<{ values: string[]; total?: number; hasMore?: boolean }> => ({
|
||||
values: [],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type {
|
|||
PendingRequestOrigin,
|
||||
ResourceSubscriptionStreamState,
|
||||
ExcludedTool,
|
||||
RequestMetadata,
|
||||
} from "./types.js";
|
||||
import {
|
||||
scanXMcpHeaderDeclarations,
|
||||
|
|
@ -356,8 +357,8 @@ function notificationMethodFromSchema(schema: unknown): string | undefined {
|
|||
interface ToolCallRequest {
|
||||
tool: Tool;
|
||||
args: Record<string, JsonValue>;
|
||||
generalMetadata?: Record<string, string>;
|
||||
toolSpecificMetadata?: Record<string, string>;
|
||||
generalMetadata?: RequestMetadata;
|
||||
toolSpecificMetadata?: RequestMetadata;
|
||||
taskOptions?: { ttl?: number };
|
||||
options?: { skipOutputValidation?: boolean };
|
||||
}
|
||||
|
|
@ -420,7 +421,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
private progress: boolean;
|
||||
private resetTimeoutOnProgress: boolean;
|
||||
private requestTimeout: number | undefined;
|
||||
private defaultMetadata: Record<string, string> | undefined;
|
||||
private defaultMetadata: RequestMetadata | undefined;
|
||||
private serverSettings: InspectorServerSettings | undefined;
|
||||
private versionNegotiation: VersionNegotiationOptions;
|
||||
private status: ConnectionStatus = "disconnected";
|
||||
|
|
@ -1035,7 +1036,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
* injecting an empty `_meta` field.
|
||||
*/
|
||||
private mergeMeta(
|
||||
callMetadata?: Record<string, string>,
|
||||
callMetadata?: RequestMetadata,
|
||||
): Record<string, string> | undefined {
|
||||
const defaults = this.defaultMetadata;
|
||||
// Modern-era per-request log level (#1629): stamp the opt-in `_meta` key on
|
||||
|
|
@ -1055,6 +1056,26 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `progressToken` a caller stamped on its request metadata, when it is
|
||||
* one the SDK will accept.
|
||||
*
|
||||
* `_meta` values are arbitrary JSON (#1910), but `progressToken` is a
|
||||
* reserved key the spec types as `string | number`. Anything else — an
|
||||
* object, an array, `null`, a boolean — is not a token the SDK can correlate
|
||||
* a `notifications/progress` back to, so it is read as "no token" rather than
|
||||
* forwarded. The key still reaches the wire inside `_meta`; this only governs
|
||||
* whether the client wires up a progress callback for it.
|
||||
*/
|
||||
private progressTokenOf(
|
||||
metadata: RequestMetadata | undefined,
|
||||
): ProgressToken | undefined {
|
||||
const token = metadata?.progressToken;
|
||||
return typeof token === "string" || typeof token === "number"
|
||||
? token
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private getRequestOptions(
|
||||
progressToken?: ProgressToken,
|
||||
signal?: AbortSignal,
|
||||
|
|
@ -1110,7 +1131,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
* so the merge/omit branch is defined once.
|
||||
*/
|
||||
private aggregateListParams(
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): { _meta: Record<string, string> } | undefined {
|
||||
const effectiveMeta = this.mergeMeta(metadata);
|
||||
return effectiveMeta ? { _meta: effectiveMeta } : undefined;
|
||||
|
|
@ -3405,7 +3426,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listTools(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ tools: Tool[]; nextCursor?: string }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -3423,7 +3444,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
this.client!.request(
|
||||
{ method: "tools/list", params },
|
||||
ListToolsResultSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
);
|
||||
const tools = [...(response.tools || [])];
|
||||
|
|
@ -3444,7 +3465,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listAllTools(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ tools: Tool[] }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -3596,7 +3617,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
* from it so every field except the entries is still validated. */
|
||||
resultSchema: z.ZodObject;
|
||||
aggregate: () => Promise<T[]>;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
/**
|
||||
* Filter applied to SALVAGED entries only, to reproduce a filter the SDK's
|
||||
* strict aggregate applies for us. Without it the fallback would return a
|
||||
|
|
@ -3674,7 +3695,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
itemsKey: string;
|
||||
itemSchema: z.ZodType<T>;
|
||||
resultSchema: z.ZodObject;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
err: unknown;
|
||||
}): Promise<T[]> {
|
||||
const pageSchema = lenientListPageSchema(resultSchema, itemsKey);
|
||||
|
|
@ -3725,7 +3746,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
: this.client!.request(
|
||||
{ method, params },
|
||||
pageSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method },
|
||||
);
|
||||
|
|
@ -3811,7 +3832,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
* stops the walk (non-converging-server guard, mirroring the SDK).
|
||||
*/
|
||||
async refreshExcludedTools(
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<ExcludedTool[]> {
|
||||
const excluded: ExcludedTool[] = [];
|
||||
// Gated to connections that actually exclude; otherwise this is a pure
|
||||
|
|
@ -3881,7 +3902,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
private async listToolsForScan(
|
||||
cursor: string | undefined,
|
||||
metadata: Record<string, string> | undefined,
|
||||
metadata: RequestMetadata | undefined,
|
||||
): Promise<{
|
||||
tools: Tool[];
|
||||
nextCursor?: string;
|
||||
|
|
@ -3924,7 +3945,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
: this.client!.request(
|
||||
{ method: "tools/list", params },
|
||||
pageSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method: "tools/list" },
|
||||
);
|
||||
|
|
@ -4016,8 +4037,8 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
async callTool(
|
||||
tool: Tool,
|
||||
args: Record<string, JsonValue>,
|
||||
generalMetadata?: Record<string, string>,
|
||||
toolSpecificMetadata?: Record<string, string>,
|
||||
generalMetadata?: RequestMetadata,
|
||||
toolSpecificMetadata?: RequestMetadata,
|
||||
taskOptions?: { ttl?: number },
|
||||
options?: { skipOutputValidation?: boolean },
|
||||
): Promise<ToolCallInvocation> {
|
||||
|
|
@ -4245,7 +4266,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
const convertedArgs = this.convertStringToolArgs(tool, args);
|
||||
|
||||
// Merge general metadata with tool-specific metadata; tool-specific wins.
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
const callMetadata: RequestMetadata | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
|
@ -4270,7 +4291,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
}
|
||||
|
||||
const requestOptions = this.getRequestOptions(
|
||||
metadata?.progressToken,
|
||||
this.progressTokenOf(metadata),
|
||||
signal,
|
||||
);
|
||||
this.applyMirroredParamHeaders(tool, convertedArgs, requestOptions);
|
||||
|
|
@ -4360,11 +4381,11 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
private dispatchFailedToolCall(
|
||||
tool: Tool,
|
||||
args: Record<string, JsonValue>,
|
||||
generalMetadata: Record<string, string> | undefined,
|
||||
toolSpecificMetadata: Record<string, string> | undefined,
|
||||
generalMetadata: RequestMetadata | undefined,
|
||||
toolSpecificMetadata: RequestMetadata | undefined,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
const callMetadata: RequestMetadata | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
|
@ -4848,8 +4869,8 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
async callToolStream(
|
||||
tool: Tool,
|
||||
args: Record<string, JsonValue>,
|
||||
generalMetadata?: Record<string, string>,
|
||||
toolSpecificMetadata?: Record<string, string>,
|
||||
generalMetadata?: RequestMetadata,
|
||||
toolSpecificMetadata?: RequestMetadata,
|
||||
taskOptions?: { ttl?: number },
|
||||
): Promise<ToolCallInvocation> {
|
||||
if (!this.client) {
|
||||
|
|
@ -4859,7 +4880,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
const convertedArgs = this.convertStringToolArgs(tool, args);
|
||||
|
||||
// Merge general metadata with tool-specific metadata; tool-specific wins.
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
const callMetadata: RequestMetadata | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
|
@ -4895,7 +4916,9 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
// attach one here either — doing so would request a progress token (and
|
||||
// emit requestorTaskProgress) for task calls only, bypassing the toggle
|
||||
// that governs every other call path.
|
||||
const requestOptions = this.getRequestOptions(metadata?.progressToken);
|
||||
const requestOptions = this.getRequestOptions(
|
||||
this.progressTokenOf(metadata),
|
||||
);
|
||||
// The task-augmented `tools/call` needs the same SEP-2243 mirroring as the
|
||||
// plain one — a strict modern server rejects it with -32020 otherwise.
|
||||
this.applyMirroredParamHeaders(tool, convertedArgs, requestOptions);
|
||||
|
|
@ -5056,7 +5079,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
return invocation;
|
||||
} catch (error) {
|
||||
// Merge general metadata with tool-specific metadata for error case
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
const callMetadata: RequestMetadata | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
|
@ -5086,7 +5109,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listResources(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ resources: Resource[]; nextCursor?: string }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5100,7 +5123,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
this.client!.request(
|
||||
{ method: "resources/list", params },
|
||||
ListResourcesResultSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
);
|
||||
return {
|
||||
|
|
@ -5117,7 +5140,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listAllResources(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ resources: Resource[] }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5148,7 +5171,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async readResource(
|
||||
uri: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<ResourceReadInvocation> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5167,7 +5190,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
"resources/read",
|
||||
params,
|
||||
ReadResourceResultSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method: "resources/read" },
|
||||
);
|
||||
|
|
@ -5198,7 +5221,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
async readResourceFromTemplate(
|
||||
uriTemplate: string,
|
||||
params: Record<string, string>,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<ResourceTemplateReadInvocation> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5260,7 +5283,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listResourceTemplates(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ resourceTemplates: ResourceTemplate[]; nextCursor?: string }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5275,7 +5298,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
this.client!.request(
|
||||
{ method: "resources/templates/list", params },
|
||||
ListResourceTemplatesResultSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method: "resources/templates/list" },
|
||||
);
|
||||
|
|
@ -5293,7 +5316,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listAllResourceTemplates(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ resourceTemplates: ResourceTemplate[] }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5324,7 +5347,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listPrompts(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ prompts: Prompt[]; nextCursor?: string }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5338,7 +5361,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
this.client!.request(
|
||||
{ method: "prompts/list", params },
|
||||
ListPromptsResultSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
);
|
||||
return {
|
||||
|
|
@ -5355,7 +5378,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
*/
|
||||
async listAllPrompts(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ prompts: Prompt[] }> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5388,7 +5411,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
async getPrompt(
|
||||
name: string,
|
||||
args?: Record<string, JsonValue>,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<PromptGetInvocation> {
|
||||
if (!this.client) {
|
||||
throw new Error("Client is not connected");
|
||||
|
|
@ -5412,7 +5435,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
"prompts/get",
|
||||
params,
|
||||
GetPromptResultSchema,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{ method: "prompts/get", toolName: name },
|
||||
);
|
||||
|
|
@ -5451,7 +5474,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
argumentName: string,
|
||||
argumentValue: string,
|
||||
context?: Record<string, string>,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ values: string[]; total?: number; hasMore?: boolean }> {
|
||||
if (!this.client) {
|
||||
return { values: [] };
|
||||
|
|
@ -5473,7 +5496,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
() =>
|
||||
this.client!.complete(
|
||||
params,
|
||||
this.getRequestOptions(metadata?.progressToken),
|
||||
this.getRequestOptions(this.progressTokenOf(metadata)),
|
||||
),
|
||||
{
|
||||
method: "completion/complete",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import type {
|
|||
ResourceTemplateReadInvocation,
|
||||
ResourceSubscriptionStreamState,
|
||||
ExcludedTool,
|
||||
RequestMetadata,
|
||||
} from "./types.js";
|
||||
import type { MalformedListItem } from "./listSalvage.js";
|
||||
import type {
|
||||
|
|
@ -112,7 +113,7 @@ export interface InspectorClientEventMap {
|
|||
timestamp: Date;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
/** Non-fatal outputSchema mismatch detected on the skipOutputValidation path. */
|
||||
outputValidationError?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import type {
|
|||
ToolCallInvocation,
|
||||
ResourceSubscriptionStreamState,
|
||||
ExcludedTool,
|
||||
RequestMetadata,
|
||||
} from "./types.js";
|
||||
import type {
|
||||
CacheMode,
|
||||
|
|
@ -82,19 +83,19 @@ export interface InspectorClientProtocol extends InspectorClientEventTarget {
|
|||
// Paginated list methods used by managed/paged state stores
|
||||
listTools(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ tools: Tool[]; nextCursor?: string }>;
|
||||
listPrompts(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ prompts: Prompt[]; nextCursor?: string }>;
|
||||
listResources(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ resources: Resource[]; nextCursor?: string }>;
|
||||
listResourceTemplates(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ resourceTemplates: ResourceTemplate[]; nextCursor?: string }>;
|
||||
listRequestorTasks(
|
||||
cursor?: string,
|
||||
|
|
@ -126,41 +127,41 @@ export interface InspectorClientProtocol extends InspectorClientEventTarget {
|
|||
// `cacheMode` uses the SDK default ('use').
|
||||
listAllTools(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ tools: Tool[] }>;
|
||||
listAllPrompts(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ prompts: Prompt[] }>;
|
||||
listAllResources(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ resources: Resource[] }>;
|
||||
listAllResourceTemplates(options?: {
|
||||
cacheMode?: CacheMode;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}): Promise<{ resourceTemplates: ResourceTemplate[] }>;
|
||||
|
||||
// Invocation methods used by paged result panels
|
||||
callTool(
|
||||
tool: Tool,
|
||||
args: Record<string, JsonValue>,
|
||||
generalMetadata?: Record<string, string>,
|
||||
toolSpecificMetadata?: Record<string, string>,
|
||||
generalMetadata?: RequestMetadata,
|
||||
toolSpecificMetadata?: RequestMetadata,
|
||||
): Promise<ToolCallInvocation>;
|
||||
readResource(
|
||||
uri: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<ResourceReadInvocation>;
|
||||
readResourceFromTemplate(
|
||||
uriTemplate: string,
|
||||
params: Record<string, string>,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<ResourceTemplateReadInvocation>;
|
||||
getPrompt(
|
||||
name: string,
|
||||
params?: Record<string, string>,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<PromptGetInvocation>;
|
||||
|
||||
// Misc surface required by hooks/state
|
||||
|
|
@ -182,6 +183,6 @@ export interface InspectorClientProtocol extends InspectorClientEventTarget {
|
|||
argumentName: string,
|
||||
argumentValue: string,
|
||||
context?: Record<string, string>,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<{ values: string[]; total?: number; hasMore?: boolean }>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function headersToServerSettings(
|
|||
return {
|
||||
headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
|
||||
env: [],
|
||||
metadata: [],
|
||||
metadata: {},
|
||||
connectionTimeout: 0,
|
||||
requestTimeout: 0,
|
||||
taskTtl: DEFAULT_TASK_TTL_MS,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
} from "../../types.js";
|
||||
import type {
|
||||
InspectorServerSettings,
|
||||
RequestMetadata,
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
StdioServerConfig,
|
||||
|
|
@ -1212,6 +1213,11 @@ export function createRemoteApp(
|
|||
// credentials are intentionally lost on first read (hard cutover per
|
||||
// #1358 decision 4). Users re-enter via the form or hand-edit into the
|
||||
// flat shape.
|
||||
// A plain JSON object — the container `_meta` requires. Values are
|
||||
// deliberately unchecked: `_meta` takes arbitrary JSON (#1910), and anything
|
||||
// that survived `JSON.parse` already is.
|
||||
const isJsonObject = (v: unknown): v is RequestMetadata =>
|
||||
v !== null && typeof v === "object" && !Array.isArray(v);
|
||||
const isStringRecord = (v: unknown): v is Record<string, string> => {
|
||||
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
||||
for (const val of Object.values(v as Record<string, unknown>)) {
|
||||
|
|
@ -1324,10 +1330,18 @@ export function createRemoteApp(
|
|||
);
|
||||
delete valObj.headers;
|
||||
}
|
||||
if ("metadata" in valObj && !isKvArray(valObj.metadata)) {
|
||||
// A JSON object post-#1910; the pre-#1910 `{ key, value }[]` pair array
|
||||
// is still accepted so an existing file keeps working (it is normalized
|
||||
// by `normalizeStoredMetadata` on the way into the settings shape, and
|
||||
// rewritten as an object on the next save).
|
||||
if (
|
||||
"metadata" in valObj &&
|
||||
!isJsonObject(valObj.metadata) &&
|
||||
!isKvArray(valObj.metadata)
|
||||
) {
|
||||
logWarn(
|
||||
{ route: "/api/servers", id, droppedKey: "metadata" },
|
||||
"Dropping malformed `metadata` field — expected `Array<{ key: string, value: string }>`.",
|
||||
"Dropping malformed `metadata` field — expected a JSON object.",
|
||||
);
|
||||
delete valObj.metadata;
|
||||
}
|
||||
|
|
@ -1556,10 +1570,14 @@ export function createRemoteApp(
|
|||
error: "settings.headers must be an array of { key, value }",
|
||||
};
|
||||
}
|
||||
if (!isKvArray(obj.metadata)) {
|
||||
// `_meta` takes any JSON, so metadata crosses the wire as a plain JSON
|
||||
// object rather than the `{ key, value }` rows headers/env still use
|
||||
// (#1910). Only the container is checked — the values are arbitrary JSON
|
||||
// by design, and anything that survived `c.req.json()` already is.
|
||||
if (!isJsonObject(obj.metadata)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "settings.metadata must be an array of { key, value }",
|
||||
error: "settings.metadata must be a JSON object",
|
||||
};
|
||||
}
|
||||
// env is optional on the wire (older clients / non-stdio servers won't send
|
||||
|
|
@ -1735,7 +1753,7 @@ export function createRemoteApp(
|
|||
// Absent → empty list, matching the read side. The write-through drops
|
||||
// empty-key rows and clears `config.env` when the list is empty.
|
||||
env: isKvArray(obj.env) ? obj.env : [],
|
||||
metadata: obj.metadata as { key: string; value: string }[],
|
||||
metadata: obj.metadata as RequestMetadata,
|
||||
connectionTimeout: obj.connectionTimeout as number,
|
||||
requestTimeout: obj.requestTimeout as number,
|
||||
// Absent → product default, matching the read side
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import type { Root } from "@modelcontextprotocol/client";
|
||||
import type {
|
||||
InspectorServerSettings,
|
||||
RequestMetadata,
|
||||
MCPConfig,
|
||||
MCPServerConfig,
|
||||
ServerEntry,
|
||||
|
|
@ -28,6 +29,7 @@ import {
|
|||
envSecretField,
|
||||
} from "../auth/secret-fields.js";
|
||||
import { toRecord } from "../json/jsonUtils.js";
|
||||
import type { JsonValue } 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.
|
||||
|
|
@ -194,6 +196,60 @@ export function envPairsToRecord(
|
|||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a server's `metadata` off disk into the in-memory `RequestMetadata`
|
||||
* object, accepting both the current object shape and the pre-#1910
|
||||
* `{ key, value }[]` pair array.
|
||||
*
|
||||
* The pair array is the shape every `mcp.json` written before #1910 carries, so
|
||||
* dropping it would silently stop sending a user's configured `_meta` on the
|
||||
* first read of an existing file. It is read here and never written back:
|
||||
* `inspectorSettingsToStoredFields` emits only the object form, so the file is
|
||||
* migrated the next time the entry is saved.
|
||||
*
|
||||
* Anything else — a string, a number, an array of non-pairs — is not metadata
|
||||
* the Inspector can send, so it is dropped with a warning rather than half-
|
||||
* interpreted, following `cleanRoots` / `cleanAuthorizationParams` above. A
|
||||
* pair whose key is blank is skipped for the same reason the write side omits
|
||||
* it: `_meta` has no meaningful empty key.
|
||||
*/
|
||||
export function normalizeStoredMetadata(
|
||||
metadata: StoredMCPServer["metadata"] | unknown,
|
||||
): RequestMetadata {
|
||||
if (metadata === undefined || metadata === null) return {};
|
||||
|
||||
if (Array.isArray(metadata)) {
|
||||
const out: RequestMetadata = {};
|
||||
for (const entry of metadata) {
|
||||
if (
|
||||
typeof entry !== "object" ||
|
||||
entry === null ||
|
||||
typeof (entry as { key?: unknown }).key !== "string"
|
||||
) {
|
||||
console.warn(
|
||||
"Ignoring malformed legacy metadata entry (expected `{ key, value }`):",
|
||||
entry,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const { key, value } = entry as { key: string; value?: JsonValue };
|
||||
if (key.trim() === "") continue;
|
||||
out[key] = value ?? "";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (typeof metadata === "object") {
|
||||
return { ...(metadata as RequestMetadata) };
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"Ignoring malformed `metadata` (expected a JSON object):",
|
||||
metadata,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a server's `oauth.authorizationParams` as it comes off disk, or
|
||||
* `undefined` when there is nothing usable. (#2018)
|
||||
|
|
@ -349,7 +405,7 @@ export function storedFieldsToInspectorSettings(
|
|||
const settings: InspectorServerSettings = {
|
||||
headers: headersPairs,
|
||||
env: envRecordToPairs(stored.env),
|
||||
metadata: stored.metadata ?? [],
|
||||
metadata: normalizeStoredMetadata(stored.metadata),
|
||||
connectionTimeout: stored.connectionTimeout ?? 0,
|
||||
requestTimeout: stored.requestTimeout ?? 0,
|
||||
// Unlike the timeouts (0 = "SDK default"), task TTL has a concrete product
|
||||
|
|
@ -458,9 +514,11 @@ export function inspectorSettingsToStoredFields(
|
|||
out.headers = headersRecord;
|
||||
}
|
||||
|
||||
const metadataFiltered = settings.metadata.filter((m) => m.key.trim() !== "");
|
||||
if (metadataFiltered.length > 0) {
|
||||
out.metadata = metadataFiltered;
|
||||
// Blank-key entries can't exist in the object form (an object has no blank
|
||||
// key the user can leave mid-edit, the way a `{key,value}` row could), so
|
||||
// there is nothing to filter — only the empty-object case to omit.
|
||||
if (Object.keys(settings.metadata).length > 0) {
|
||||
out.metadata = settings.metadata;
|
||||
}
|
||||
|
||||
if (settings.connectionTimeout > 0) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type {
|
|||
} from "@modelcontextprotocol/client";
|
||||
import { isClientDecodeRejection } from "../listSalvage.js";
|
||||
import { isTerminalStatus } from "../types.js";
|
||||
import type { RequestMetadata } from "../types.js";
|
||||
import { TypedEventTarget } from "../typedEventTarget.js";
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +68,7 @@ export interface ManagedListConfig<T, M extends ManagedListEventMap> {
|
|||
fetchAll: (
|
||||
client: InspectorClientProtocol,
|
||||
cacheMode: CacheMode | undefined,
|
||||
metadata: Record<string, string> | undefined,
|
||||
metadata: RequestMetadata | undefined,
|
||||
) => Promise<T[]>;
|
||||
/**
|
||||
* Whether this list drives a list-changed indicator. When true, a
|
||||
|
|
@ -97,7 +98,7 @@ export abstract class ManagedListState<
|
|||
protected items: T[] = [];
|
||||
protected client: InspectorClientProtocol | null = null;
|
||||
private unsubscribe: (() => void) | null = null;
|
||||
private _metadata: Record<string, string> | undefined = undefined;
|
||||
private _metadata: RequestMetadata | undefined = undefined;
|
||||
private listChanged = false;
|
||||
// The last fetch's failure, kept as observable state so a load that fails
|
||||
// (a transport error, or a result the SDK codec rejects as invalid) is
|
||||
|
|
@ -282,7 +283,7 @@ export abstract class ManagedListState<
|
|||
this.emit("errorChange", value);
|
||||
}
|
||||
|
||||
setMetadata(metadata?: Record<string, string>): void {
|
||||
setMetadata(metadata?: RequestMetadata): void {
|
||||
this._metadata = metadata;
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +300,7 @@ export abstract class ManagedListState<
|
|||
* the `list_changed` auto-refresh) catch it explicitly (#1953).
|
||||
*/
|
||||
async refresh(
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
cacheMode?: CacheMode,
|
||||
): Promise<T[]> {
|
||||
let next: T[] | null;
|
||||
|
|
@ -336,7 +337,7 @@ export abstract class ManagedListState<
|
|||
* the console; empty list is the right semantics).
|
||||
*/
|
||||
private async fetchItems(
|
||||
metadata: Record<string, string> | undefined,
|
||||
metadata: RequestMetadata | undefined,
|
||||
cacheMode: CacheMode | undefined,
|
||||
): Promise<T[] | null> {
|
||||
const client = this.client;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import type { InspectorClientProtocol } from "../inspectorClientProtocol.js";
|
||||
import type { Prompt } from "@modelcontextprotocol/client";
|
||||
import { isTerminalStatus } from "../types.js";
|
||||
import type { RequestMetadata } from "../types.js";
|
||||
import { TypedEventTarget } from "../typedEventTarget.js";
|
||||
import type { PagePaginationState } from "./pagedToolsState.js";
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ export class PagedPromptsState extends TypedEventTarget<PagedPromptsStateEventMa
|
|||
|
||||
async loadPage(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<LoadPageResult> {
|
||||
const c = this.client;
|
||||
if (!c || c.getStatus() !== "connected") {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import type { InspectorClientProtocol } from "../inspectorClientProtocol.js";
|
||||
import type { ResourceTemplateType as ResourceTemplate } from "@modelcontextprotocol/client";
|
||||
import { isTerminalStatus } from "../types.js";
|
||||
import type { RequestMetadata } from "../types.js";
|
||||
import { TypedEventTarget } from "../typedEventTarget.js";
|
||||
|
||||
export interface PagedResourceTemplatesStateEventMap {
|
||||
|
|
@ -59,7 +60,7 @@ export class PagedResourceTemplatesState extends TypedEventTarget<PagedResourceT
|
|||
|
||||
async loadPage(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<LoadPageResult> {
|
||||
const c = this.client;
|
||||
if (!c || c.getStatus() !== "connected") {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import type { InspectorClientProtocol } from "../inspectorClientProtocol.js";
|
||||
import type { Resource } from "@modelcontextprotocol/client";
|
||||
import { isTerminalStatus } from "../types.js";
|
||||
import type { RequestMetadata } from "../types.js";
|
||||
import { TypedEventTarget } from "../typedEventTarget.js";
|
||||
import type { PagePaginationState } from "./pagedToolsState.js";
|
||||
|
||||
|
|
@ -113,7 +114,7 @@ export class PagedResourcesState extends TypedEventTarget<PagedResourcesStateEve
|
|||
|
||||
async loadPage(
|
||||
cursor?: string,
|
||||
metadata?: Record<string, string>,
|
||||
metadata?: RequestMetadata,
|
||||
): Promise<LoadPageResult> {
|
||||
const c = this.client;
|
||||
if (!c || c.getStatus() !== "connected") {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type { OAuthClientProvider } from "@modelcontextprotocol/client";
|
|||
import type { Transport } from "@modelcontextprotocol/client";
|
||||
import type { InspectorLogger } from "../logging/logger.js";
|
||||
import type { AppElicitationRenderer } from "./appElicitation.js";
|
||||
import type { JsonValue } from "../json/jsonUtils.js";
|
||||
import type { JsonObject, JsonValue } from "../json/jsonUtils.js";
|
||||
import type {
|
||||
ClientConfig,
|
||||
EnterpriseManagedAuthIdpConfig,
|
||||
|
|
@ -99,11 +99,20 @@ export type StoredMCPServer = MCPServerConfig & {
|
|||
*/
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* Default `_meta` keys merged into every outgoing MCP request. Inspector-
|
||||
* specific (no analog in the broader mcp.json ecosystem), so the pair-array
|
||||
* shape is preserved on disk and in memory.
|
||||
* Default `_meta` payload merged into every outgoing MCP request. Inspector-
|
||||
* specific (no analog in the broader mcp.json ecosystem).
|
||||
*
|
||||
* A JSON object, so a value may be any JSON — object, array, number, boolean,
|
||||
* `null` — not just a string (#1910). Nothing in the MCP spec restricts
|
||||
* `_meta` to string values, and the SDK models it as a passthrough object;
|
||||
* the string-pair restriction was the Inspector's own.
|
||||
*
|
||||
* The pre-#1910 on-disk shape was a `{ key, value }[]` pair array. It is
|
||||
* still **read** (see `normalizeStoredMetadata` in `serverList.ts`) so an
|
||||
* existing `mcp.json` keeps working, but it is never written back — a
|
||||
* round-trip through the Inspector rewrites the field as an object.
|
||||
*/
|
||||
metadata?: { key: string; value: string }[];
|
||||
metadata?: JsonObject;
|
||||
/**
|
||||
* Protocol era to negotiate with this server (`"legacy" | "auto" | "modern"`),
|
||||
* orthogonal to the transport `type`. Inspector-specific (no analog in the
|
||||
|
|
@ -361,6 +370,19 @@ export interface ServerState {
|
|||
stderrLogs: StderrLogEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A `_meta` payload: a JSON object whose values may be any JSON, not just
|
||||
* strings (#1910).
|
||||
*
|
||||
* The MCP spec places no restriction on `_meta` value types and the SDK models
|
||||
* the field as a passthrough object, so the Inspector must be able to send —
|
||||
* and to record — nested objects, arrays, numbers, booleans and `null`. Used
|
||||
* for both the per-server default payload (`InspectorServerSettings.metadata`,
|
||||
* `InspectorClientOptions.defaultMetadata`) and the per-call metadata every
|
||||
* request verb accepts.
|
||||
*/
|
||||
export type RequestMetadata = Record<string, JsonValue>;
|
||||
|
||||
/**
|
||||
* Represents a complete resource read invocation, including request parameters,
|
||||
* response, and metadata.
|
||||
|
|
@ -369,7 +391,7 @@ export interface ResourceReadInvocation {
|
|||
result: ReadResourceResult;
|
||||
timestamp: Date;
|
||||
uri: string;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -382,7 +404,7 @@ export interface ResourceTemplateReadInvocation {
|
|||
result: ReadResourceResult;
|
||||
timestamp: Date;
|
||||
params: Record<string, string>;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -394,7 +416,7 @@ export interface PromptGetInvocation {
|
|||
timestamp: Date;
|
||||
name: string;
|
||||
params?: Record<string, string>;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -408,7 +430,7 @@ export interface ToolCallInvocation {
|
|||
timestamp: Date;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
metadata?: Record<string, string>;
|
||||
metadata?: RequestMetadata;
|
||||
/**
|
||||
* Set only on the `skipOutputValidation` path: present when the (delivered)
|
||||
* result's structuredContent does NOT match the tool's declared outputSchema.
|
||||
|
|
@ -689,7 +711,12 @@ export function eraToVersionNegotiation(
|
|||
*/
|
||||
export interface InspectorServerSettings {
|
||||
headers: { key: string; value: string }[];
|
||||
metadata: { key: string; value: string }[];
|
||||
/**
|
||||
* Default `_meta` payload for this server, edited as a JSON object rather
|
||||
* than key/value rows so a value can be any JSON (#1910). Always present;
|
||||
* `{}` means "send no default metadata".
|
||||
*/
|
||||
metadata: RequestMetadata;
|
||||
/**
|
||||
* Environment variables for stdio servers, edited as controlled key/value
|
||||
* rows (mirrors `headers`). Only meaningful for stdio transports; non-stdio
|
||||
|
|
@ -1075,7 +1102,7 @@ export interface InspectorClientOptions {
|
|||
* site metadata wins on key collision. Set this from `InspectorServerSettings.metadata`
|
||||
* so persisted server-wide metadata reaches the wire on the first request.
|
||||
*/
|
||||
defaultMetadata?: Record<string, string>;
|
||||
defaultMetadata?: RequestMetadata;
|
||||
|
||||
/**
|
||||
* Optional per-server runtime settings forwarded to the transport factory
|
||||
|
|
|
|||
Loading…
Reference in a new issue