Skip to content

Enum constraints from MCP tool schemas are dropped #38

Description

@dimabullet

Summary

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.

src/Relay.php

$type = data_get($property, 'type');

return match ($type) {
    'string' => new StringSchema($name, $description),   // enum is dropped here
    'enum'   => new EnumSchema($name, $description, data_get($property, 'options', [])),
    ...
};

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:

public function schema(JsonSchema $schema): array
{
    return [
        'status' => $schema->string()
            ->enum(['open', 'closed'])
            ->description('The status to filter on'),
    ];
}

Then resolve it:

$tool = Relay::tools('my-server')[0];

$tool->parameters()['status'];
// Prism\Prism\Schema\StringSchema  <- expected EnumSchema, options are gone

Impact

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions