fix(mcp): keep detail guidance in compact input schemas (#8034)

Compact input schemas are the default advertisement since #5975, and the
profile drops every property description. On 23 of the 31 tools with a
guided `detail` option, including inspect_artifact and inspect_macho,
`detail` is advertised as a bare complete|summary enum, so callers take the
complete default. On a universal macOS app that reply was about 699k
characters for inspect_artifact and 336k for inspect_macho, against about
4.4k and 3.2k with summary.

Keep the canonical description of a root `detail` property in the inline
compact input form. The reduced form already keeps first-level guidance, and
compact output schemas are unchanged. Advertised compact input schemas grow
from 127,616 to 133,358 bytes across 233 tools; the largest schema stays at
4,107 bytes, within the 13,312-byte budget.

The new boundary test compares `detail` guidance between the full and compact
tools/list and fails before the change.
This commit is contained in:
Christopher Pruijsen authored and GitHub committed 2026-11-04 19:14:52 +08:00
1 parent a56cc93cab
commit 5b953e13c4
3 files changed
+62 -2

No files matched your search

+3 -1
View File
@@ -1707,7 +1707,9 @@ JSON Schema:
- Annotation prose leaves the advertised form: nested property descriptions,
literal examples, defaults, and titles are dropped. The root keeps its
description because it states the accepted input groups.
description because it states the accepted input groups. A root `detail`
property keeps its description because it selects between complete Evidence
and a bounded summary.
- Schema-local references are inlined within the budget. Kimi Code expands
references before sending provider requests, so sharing definitions cannot
establish the provider-facing size. Expansion stops at the budget before
+32 -1
View File
@@ -2,6 +2,7 @@ import {
COMPACT_SCHEMA_MAX_DEPTH,
hasGatewayIncompatibleShape,
inlineWithinBudget,
isSchemaObject,
reduceToShallowProperties,
schemaDepth,
serializedBytes,
@@ -28,6 +29,34 @@ export interface CompactedInputSchema {
const INPUT_REDUCED_ANNOTATION =
"Advertised in reduced form by the compact input schema profile; the server still validates complete canonical inputs.";
// `detail` selects between complete Evidence and a bounded summary. Without
// its guidance, callers default to complete replies that can exceed context.
const retainedRootPropertyDescriptions = ["detail"];
/** Restore the description of root properties whose choice bounds the reply. */
const retainRootPropertyGuidance = (
schema: Record<string, unknown>,
stripped: Record<string, unknown>,
): Record<string, unknown> => {
if (
!isSchemaObject(schema.properties) ||
!isSchemaObject(stripped.properties)
)
return stripped;
const properties = { ...stripped.properties };
for (const name of retainedRootPropertyDescriptions) {
const source = schema.properties[name];
const kept = properties[name];
if (
isSchemaObject(source) &&
isSchemaObject(kept) &&
typeof source.description === "string"
)
properties[name] = { ...kept, description: source.description };
}
return { ...stripped, properties };
};
/**
* Render an advertised input schema within a per-schema byte budget for
* providers that validate the literal serialized schema size. Validation
@@ -41,7 +70,9 @@ export const compactAdvertisedInputSchema = (
): CompactedInputSchema => {
const expanded = inlineWithinBudget(stripAnnotations(schema), budgetBytes);
const inline =
expanded === undefined ? undefined : typeArrayItemUnions(expanded);
expanded === undefined
? undefined
: typeArrayItemUnions(retainRootPropertyGuidance(schema, expanded));
if (
inline !== undefined &&
serializedBytes(inline) <= budgetBytes &&
@@ -644,3 +644,30 @@ it.each(["full", "compact"] as const)(
expect(calls).toBe(1);
},
);
it("keeps root detail guidance so callers can choose bounded summaries", async () => {
const full = fullServer();
const compact = compactServer();
registerCatalog(full);
registerCatalog(compact);
const [fullTools, compactTools] = await Promise.all([
listToolsThroughTransport(full),
listToolsThroughTransport(compact),
]);
const detailGuidance = (tools: readonly AdvertisedTool[]) =>
Object.fromEntries(
tools.flatMap(({ name, inputSchema }) => {
const detail = (
inputSchema.properties as Record<string, Record<string, unknown>>
)?.detail;
return typeof detail?.description === "string"
? [[name, detail.description]]
: [];
}),
);
const expected = detailGuidance(fullTools);
expect(Object.keys(expected)).toEqual(
expect.arrayContaining(["inspect_artifact", "inspect_macho"]),
);
expect(detailGuidance(compactTools)).toEqual(expected);
});