Skip to content

Allow tools to declare their name dynamically#420

Open
seankndy wants to merge 1 commit intolaravel:0.xfrom
seankndy:feat/resolve-tool-name
Open

Allow tools to declare their name dynamically#420
seankndy wants to merge 1 commit intolaravel:0.xfrom
seankndy:feat/resolve-tool-name

Conversation

@seankndy
Copy link
Copy Markdown

Gateways currently emit class_basename($tool) as the name sent to providers, and InvokesTools::findTool looks tools up by the same basename. Adapter-style tools (a single class that dispatches to many distinct targets per instance) all collapse to the same basename, so providers see duplicate names and cannot route calls correctly.

Add a shared ResolvesToolName trait that honors an optional public name() method on the tool, falling back to class_basename when absent. The existing Prism gateway already used this pattern; native gateways now follow suit via the shared helper.

Covered gateways: Anthropic, AzureOpenAi, DeepSeek, Gemini, Groq, Mistral, Ollama, OpenAi, OpenRouter, Xai, plus the shared InvokesTools::findTool lookup.

Dynamic tool use-case example:

readonly class McpToolAdapter implements Tool
{
    /**
     * @param  array<string, mixed>  $inputSchema
     */
    public function __construct(
        private McpClient $client,
        private string $toolName,
        private string $toolDescription,
        private array $inputSchema,
        private string $serverSlug,
    ) {}

    /**
     * Get the tool's MCP-server-namespaced name
     */
    public function name(): string
    {
        return "{$this->serverSlug}__{$this->toolName}";
    }

    /**
     * Get the description of the tool's purpose.
     */
    public function description(): Stringable|string
    {
        return $this->toolDescription;
    }

    /**
     * Get the tool's schema definition.
     */
    public function schema(JsonSchema $schema): array
    {
        return JsonSchemaConverter::convert($this->inputSchema, $schema);
    }

    /**
     * Execute the tool by calling it on the remote MCP server.
     */
    public function handle(Request $request): Stringable|string
    {
        try {
            return $this->client->callTool($this->toolName, $request->all());
        } catch (McpServerException $e) {
            return "Error: {$e->getMessage()}";
        }
    }
}

Gateways currently emit `class_basename($tool)` as the name sent to
providers, and `InvokesTools::findTool` looks tools up by the same
basename. Adapter-style tools (a single class that dispatches to many
distinct targets per instance) all collapse to the same basename, so
providers see duplicate names and cannot route calls correctly.

Add a shared `ResolvesToolName` trait that honors an optional public
`name()` method on the tool, falling back to `class_basename` when
absent. The existing Prism gateway already used this pattern inline;
native gateways now follow suit via the shared helper.

Covered gateways: Anthropic, AzureOpenAi, DeepSeek, Gemini, Groq,
Mistral, Ollama, OpenAi, OpenRouter, Xai, plus the shared
`InvokesTools::findTool` lookup.
@CodeWrap
Copy link
Copy Markdown
Contributor

This fixes the exact issue we ran into on our side. We have an OfficeToolWrapper that exposes 13 distinct tools via name() (edit_slide, insert_icon, etc.), but class_basename() collapses all of them to OfficeToolWrapper, which makes Anthropic reject the tool list as non-unique. We patched our fork with the same method_exists approach a few days ago, and it's been working well. Glad you got this up first.

@pushpak1300 pushpak1300 self-requested a review April 18, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants