Skip to content

Fix async MCP tool execution in Builder App wrapper#526

Open
philsalm wants to merge 1 commit into
databricks-solutions:mainfrom
philsalm:fix-async-mcp-tool-execution
Open

Fix async MCP tool execution in Builder App wrapper#526
philsalm wants to merge 1 commit into
databricks-solutions:mainfrom
philsalm:fix-async-mcp-tool-execution

Conversation

@philsalm
Copy link
Copy Markdown

Summary

The Builder App's MCP tool wrapper at databricks-builder-app/server/services/databricks_tools.py:292 assumed all FastMCP tools were sync. But the mcp-server's _patch_tool_decorator_for_async() (in databricks-mcp-server/databricks_mcp_server/server.py) intercepts @mcp.tool registration and converts every sync function to async via _wrap_sync_in_thread. After that patch, every tool is async.

The Builder App's wrapper called ctx.run(fn, **parsed_args) inside a thread pool. For an async fn, that returns the coroutine object instead of awaiting it. The thread completes immediately with the coroutine object as its result. The agent receives the stringified coroutine repr (~50 bytes) as the tool output, doesn't know what to make of it, and either loops or stalls.

Reproduction

Deploy databricks-builder-app from main against any workspace. Open a project and ask the agent to do anything that touches an MCP tool — e.g. "What tables are in catalog X?". You'll see in the app logs:

[MCP TOOL] execute_sql called with args: {...}
[MCP TOOL] Running execute_sql in thread pool with heartbeat...
[MCP TOOL] execute_sql completed in 0.00s, result length: 50
RuntimeWarning: coroutine 'execute_sql' was never awaited

result length: 50 is the length of str(<coroutine object execute_sql at 0x...>). The agent then has no useful tool output to act on, and downstream behavior is unpredictable.

Fix

Detect async functions with inspect.iscoroutinefunction() and run them inside a fresh event loop inside the thread — same isolation pattern EVENT_LOOP_FIX.md already uses for the agent itself. Sync functions follow the original path.

if inspect.iscoroutinefunction(fn):
    def run_in_context():
        def runner():
            new_loop = asyncio.new_event_loop()
            asyncio.set_event_loop(new_loop)
            try:
                return new_loop.run_until_complete(fn(**parsed_args))
            finally:
                new_loop.close()
        return ctx.run(runner)
else:
    def run_in_context():
        return ctx.run(fn, **parsed_args)

Test plan

  • Verified the bug on a fresh deploy from main against an internal workspace — every MCP tool call returns 50 bytes with the coroutine-not-awaited warning, and the agent stalls.
  • Applied the patch and redeployed. MCP tool calls now return real results; the agent completes tasks end-to-end.
  • (Maintainers) CI / unit tests, if applicable for databricks_tools.py.

Related

  • Root cause is the mismatch with the _patch_tool_decorator_for_async introduced in databricks-mcp-server/databricks_mcp_server/server.py.
  • Comment on line 292 ("FastMCP tools are sync") is now outdated and is replaced.

This pull request and its description were written by Isaac.

The mcp-server's _patch_tool_decorator_for_async() converts every sync
@mcp.tool registration into an async function via _wrap_sync_in_thread.
The Builder App's wrapper still treated tools as sync and called
ctx.run(fn, **args) directly inside a thread pool. For an async fn this
returns the coroutine object instead of awaiting it, producing a ~50-char
stringified coroutine repr as the "result" — which then breaks any agent
that depends on tool output (execute_sql, get_table_stats_and_schema,
manage_workspace_files, etc.).

Symptoms: agent receives "result length: 50" with a RuntimeWarning:
"coroutine 'execute_sql' was never awaited", then loops or stalls because
it has no usable tool output.

Fix: detect async functions with inspect.iscoroutinefunction() and run
them inside a fresh event loop inside the thread — same isolation pattern
EVENT_LOOP_FIX.md uses for the agent itself.

Co-authored-by: Isaac
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.

1 participant