Skip to content

Bug: _buildNativeTools crashes with TypeError when MCP tools are present in toolImplementations #469

@buger

Description

@buger

Summary

ProbeAgent._buildNativeTools() throws a TypeError: Cannot destructure property 'schema' of 'this._getToolSchemaAndDescription(...)' as it is null when MCP tools have been injected into this.toolImplementations. This regression was introduced in v0.6.0-rc265.

Error

TypeError: Cannot destructure property 'schema' of 'this._getToolSchemaAndDescription(...)' as it is null.
    at _ProbeAgent._buildNativeTools (dist/index.js:354370:19)
    at _ProbeAgent.answer (dist/index.js:355596:31)

Steps to Reproduce

  1. Configure ProbeAgent with MCP tools (e.g., via an SSE or stdio MCP server)
  2. Call ProbeAgent.answer() with any prompt
  3. The agent initializes MCP, injects MCP tools into toolImplementations, then crashes when _buildNativeTools iterates them

Example configuration that triggers the crash — any MCP tools configuration works:

tools:
  - id: slack-send-dm
    type: mcp
    # ...
  - id: slack-search
    type: mcp
    # ...

Debug output confirms the MCP tools are successfully loaded into toolImplementations:

[DEBUG] All Tools Initialized
[DEBUG] Native tools: 2, MCP tools: 4
[DEBUG] Available tools:
[DEBUG]   - analyze_all
[DEBUG]   - readImage
[DEBUG]   - __tools___slack-send-dm (MCP)
[DEBUG]   - __tools___slack-search (MCP)
[DEBUG]   - __tools___slack-read-thread (MCP)
[DEBUG]   - __tools___slack-download-file (MCP)

The crash happens immediately after, when answer() calls _buildNativeTools().

Root Cause Analysis

There are two code paths that interact incorrectly:

1. MCP tool injection into toolImplementations (during initializeMCP)

When MCP is enabled, MCP tools are added to this.toolImplementations alongside native tools:

// During initializeMCP():
const mcpTools = this.mcpBridge.mcpTools || {};
for (const [toolName, toolImpl] of Object.entries(mcpTools)) {
    if (this._isMcpToolAllowed(toolName)) {
        this.toolImplementations[toolName] = toolImpl;  // MCP tools added here
    }
}

After this, toolImplementations contains both native tools (analyze_all, readImage, etc.) and MCP tools (__tools___slack-send-dm, etc.).

2. _buildNativeTools() iterates ALL toolImplementations

// In _buildNativeTools():
for (const [toolName, toolImpl] of Object.entries(this.toolImplementations)) {
    const { schema, description } = this._getToolSchemaAndDescription(toolName); // 💥 CRASH
    if (schema && description) {
        nativeTools[toolName] = wrapTool(toolName, schema, description, toolImpl.execute);
    }
}

3. _getToolSchemaAndDescription() only knows about native tools

_getToolSchemaAndDescription(toolName) {
    const toolMap = {
        search: { schema: searchSchema, description: "..." },
        query: { schema: querySchema, description: "..." },
        extract: { schema: extractSchema, description: "..." },
        // ... other native tools only
        readImage: { schema: readImageSchema, description: "..." },
        task: { schema: taskSchema, description: "..." },
    };
    return toolMap[toolName] || null;  // Returns null for MCP tools
}

When an MCP tool name like __tools___slack-send-dm is looked up, the method returns null. Destructuring { schema, description } from null throws the TypeError.

Note: Line 354371 has if (schema && description) which was intended to guard against unknown tools, but the destructuring on the previous line crashes before reaching that guard.

Suggested Fix

Replace the unsafe destructuring with a null check:

// In _buildNativeTools(), change:
for (const [toolName, toolImpl] of Object.entries(this.toolImplementations)) {
    const { schema, description } = this._getToolSchemaAndDescription(toolName);
    if (schema && description) {
        nativeTools[toolName] = wrapTool(toolName, schema, description, toolImpl.execute);
    }
}

// To:
for (const [toolName, toolImpl] of Object.entries(this.toolImplementations)) {
    const toolInfo = this._getToolSchemaAndDescription(toolName);
    if (!toolInfo) continue;
    const { schema, description } = toolInfo;
    nativeTools[toolName] = wrapTool(toolName, schema, description, toolImpl.execute);
}

This is safe because MCP tools are already handled separately further down in the same method:

// MCP tools are wired up here, after the native tool loop:
if (this.mcpBridge && !options._disableTools) {
    const mcpTools = this.mcpBridge.getVercelTools(this._filterMcpTools(this.mcpBridge.getToolNames()));
    for (const [name, mcpTool] of Object.entries(mcpTools)) {
        // ...
    }
}

An alternative (or complementary) fix would be to not add MCP tools to toolImplementations in the first place, since they're consumed through mcpBridge.getVercelTools() in _buildNativeTools. The dual registration (in toolImplementations AND via mcpBridge) seems like the root design issue.

Environment

  • Probe version: 0.6.0-rc265 (works fine on 0.6.0-rc263)
  • Runtime: Node.js, bundled via ncc
  • Trigger: Any ProbeAgent usage with MCP tools configured

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternal

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions