You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Enum constraints from MCP tool schemas are dropped #38
Enum constraints on MCP tool parameters never reach the model. getSchemeParameter() resolves a property by matching on type alone, so a standard JSON Schema enum is turned into a plain StringSchema and the allowed values are dropped.
MCP servers emit standard JSON Schema, where an enum is a string carrying an enum keyword:
{ "type": "string", "enum": ["open", "closed"] }
The 'enum' branch expects {"type": "enum", "options": [...]}, which is not JSON Schema and which no MCP server can produce, so that branch is effectively unreachable. Enums nested inside array items are lost the same way.
Worth noting Prism's own EnumSchema::toArray() serialises to {"description": ..., "enum": [...], "type": "string"}, so Relay cannot even read back what Prism itself writes.
Steps to reproduce
Expose an MCP tool with an enum parameter, for example with laravel/mcp:
publicfunctionschema(JsonSchema$schema): array
{
return [
'status' => $schema->string()
->enum(['open', 'closed'])
->description('The status to filter on'),
];
}
The model receives a free text parameter with no allowed values, so it invents them. In our case a directory search tool was called with categories that do not exist, the filters were silently ignored, and the answer was built from an unfiltered result set.
The only workaround is to repeat every allowed value in the parameter description, which Relay does preserve. That means each MCP server has to duplicate its own enums into prose and pay the extra tokens on every request.
Possible fix
Resolve the enum keyword before the type based matching. Happy to send a PR, it is a small change.
Confirming this on relay v1.8.0, and it bites in production.
Our MCP server (TypeScript SDK, zod 4) emits exactly the shape described. z.enum(["id","-id","email","-email"]) goes over the wire as:
{"description":"Sort by id or email only; prefix with - for descending.","type":"string","enum":["id","-id","email","-email"]}
getSchemeParameter() matches type: "string" first, so the tool definition the model receives has sort as a bare string with no allowed values. Symptom, from a real agent: asked for "the last 20 customers", Claude Haiku sends sort: "-created_at" on a cold conversation essentially every time. The server rejects it with the enum list, and unless the model retries, the turn is lost. The same MCP server behaves correctly in Claude Code and Claude Desktop, which read the raw input schema — so the failure is specific to the Relay conversion.
Two things worth adding:
Enums nested in array items are lost by the same line: the recursive call re-enters the same match.
The conversion drops more than enums. type: "integer" is emitted as number, and minimum / maximum / multipleOf / pattern / format / minItems never survive, even though NumberSchema and StringSchema accept them — the match arms construct every schema with (name, description) only. So a limit documented as 1..100 reaches the model as an unbounded number. Happy to open that separately if you'd prefer it tracked on its own.
We have patched locally by overriding getSchemeParameter() in a Relay subclass, which is effectively #39. Would be good to see that merged.
Activity
ftrudeau-pelcro commentedon Aug 22, 2026
Confirming this on relay v1.8.0, and it bites in production.
Our MCP server (TypeScript SDK, zod 4) emits exactly the shape described.
z.enum(["id","-id","email","-email"])goes over the wire as:{"description":"Sort by id or email only; prefix with - for descending.","type":"string","enum":["id","-id","email","-email"]}getSchemeParameter()matchestype: "string"first, so the tool definition the model receives hassortas a bare string with no allowed values. Symptom, from a real agent: asked for "the last 20 customers", Claude Haiku sendssort: "-created_at"on a cold conversation essentially every time. The server rejects it with the enum list, and unless the model retries, the turn is lost. The same MCP server behaves correctly in Claude Code and Claude Desktop, which read the raw input schema — so the failure is specific to the Relay conversion.Two things worth adding:
itemsare lost by the same line: the recursive call re-enters the samematch.type: "integer"is emitted asnumber, andminimum/maximum/multipleOf/pattern/format/minItemsnever survive, even thoughNumberSchemaandStringSchemaaccept them — thematcharms construct every schema with(name, description)only. So alimitdocumented as 1..100 reaches the model as an unbounded number. Happy to open that separately if you'd prefer it tracked on its own.We have patched locally by overriding
getSchemeParameter()in a Relay subclass, which is effectively #39. Would be good to see that merged.