mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Reject unsupported MCP elicitation forms
This commit is contained in:
parent
9616fe588d
commit
03a026512e
3 changed files with 38 additions and 12 deletions
|
|
@ -159,6 +159,8 @@ export function appServerMcpElicitationRequest(request: AppServerRequest): Pendi
|
|||
},
|
||||
};
|
||||
}
|
||||
const fields = mcpElicitationFieldsFromRequestedSchema(params.requestedSchema);
|
||||
if (!fields) return null;
|
||||
return {
|
||||
requestId: request.id,
|
||||
params: {
|
||||
|
|
@ -168,7 +170,7 @@ export function appServerMcpElicitationRequest(request: AppServerRequest): Pendi
|
|||
mode: "form",
|
||||
message: params.message,
|
||||
meta: params.meta,
|
||||
fields: mcpElicitationFieldsFromRequestedSchema(params.requestedSchema),
|
||||
fields,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -604,17 +606,19 @@ function normalizeMcpElicitationParams(params: McpElicitationParams): Normalized
|
|||
}
|
||||
}
|
||||
|
||||
function mcpElicitationFieldsFromRequestedSchema(schema: unknown): PendingMcpElicitationField[] {
|
||||
function mcpElicitationFieldsFromRequestedSchema(schema: unknown): PendingMcpElicitationField[] | null {
|
||||
const record = asRecordOrNull(schema);
|
||||
const properties = asRecordOrNull(record?.["properties"]);
|
||||
if (!properties) return [];
|
||||
if (record?.["type"] !== "object" || !properties) return null;
|
||||
const required = new Set(
|
||||
Array.isArray(record?.["required"]) ? record["required"].filter((item): item is string => typeof item === "string") : [],
|
||||
Array.isArray(record["required"]) ? record["required"].filter((item): item is string => typeof item === "string") : [],
|
||||
);
|
||||
return Object.entries(properties).flatMap(([id, fieldSchema]) => {
|
||||
if (!isMcpElicitationPrimitiveSchema(fieldSchema)) return [];
|
||||
return [mcpElicitationFieldFromSchema(id, fieldSchema, required.has(id))];
|
||||
});
|
||||
const fields: PendingMcpElicitationField[] = [];
|
||||
for (const [id, fieldSchema] of Object.entries(properties)) {
|
||||
if (!isMcpElicitationPrimitiveSchema(fieldSchema)) return null;
|
||||
fields.push(mcpElicitationFieldFromSchema(id, fieldSchema, required.has(id)));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function isMcpElicitationPrimitiveSchema(schema: unknown): schema is AppServerMcpElicitationPrimitiveSchema {
|
||||
|
|
|
|||
|
|
@ -103,8 +103,12 @@ describe("MCP elicitation request model", () => {
|
|||
expect(input.params.fields).toEqual([expect.objectContaining({ id: "title", type: "string" })]);
|
||||
});
|
||||
|
||||
it("normalizes malformed schema fields without leaking invalid values", () => {
|
||||
const input = expectPresent(toPendingMcpElicitation(malformedSchemaRequest()));
|
||||
it("rejects a form instead of dropping unsupported schema fields", () => {
|
||||
expect(toPendingMcpElicitation(malformedSchemaRequest())).toBeNull();
|
||||
});
|
||||
|
||||
it("normalizes malformed primitive schema values without leaking them", () => {
|
||||
const input = expectPresent(toPendingMcpElicitation(malformedSchemaRequest({ includeUnsupported: false })));
|
||||
if (input.params.mode !== "form") throw new Error("Expected form mode");
|
||||
|
||||
expect(input.params.fields.map((field) => field.id)).toEqual(["badDefault", "brokenSelect", "enumSelect", "labels"]);
|
||||
|
|
@ -217,7 +221,7 @@ function openAiFormRequest(): ServerRequest {
|
|||
};
|
||||
}
|
||||
|
||||
function malformedSchemaRequest(): ServerRequest {
|
||||
function malformedSchemaRequest({ includeUnsupported = true }: { includeUnsupported?: boolean } = {}): ServerRequest {
|
||||
return {
|
||||
id: 44,
|
||||
method: "mcpServer/elicitation/request",
|
||||
|
|
@ -230,8 +234,9 @@ function malformedSchemaRequest(): ServerRequest {
|
|||
message: "Provide issue details",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
required: includeUnsupported ? ["unsupported"] : [],
|
||||
properties: {
|
||||
unsupported: { type: "object", title: "Unsupported" },
|
||||
...(includeUnsupported ? { unsupported: { type: "object", title: "Unsupported" } } : {}),
|
||||
badDefault: { type: "boolean", title: "Bad default", default: "yes" },
|
||||
brokenSelect: { type: "string", title: "Broken select", oneOf: { const: "low", title: "Low" } },
|
||||
enumSelect: { type: "string", title: "Enum select", enum: ["low", 1, "high"] },
|
||||
|
|
|
|||
|
|
@ -119,6 +119,23 @@ describe("chat inbound routing", () => {
|
|||
expectRequestRouteKind({ ...request, params: { ...request.params, threadId: "thread-other" } } as ServerRequest, "inactive");
|
||||
});
|
||||
|
||||
it("routes MCP forms with unsupported fields to diagnostic rejection", () => {
|
||||
const request = mcpElicitationRequest();
|
||||
const unsupported = {
|
||||
...request,
|
||||
params: {
|
||||
...request.params,
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
required: ["nested"],
|
||||
properties: { nested: { type: "object" } },
|
||||
},
|
||||
},
|
||||
} as ServerRequest;
|
||||
|
||||
expectRequestRouteKind(unsupported, "unsupported");
|
||||
});
|
||||
|
||||
it("marks scoped messages inactive when the thread or turn does not match", () => {
|
||||
const otherThread = {
|
||||
method: "item/agentMessage/delta",
|
||||
|
|
|
|||
Loading…
Reference in a new issue