fix(mcp): Restore synchronous dispatch and update mcp_server to use async_dispatch.

This commit is contained in:
2026-03-06 14:03:41 -05:00
parent 9d5b874c66
commit 5a484c9e82
2 changed files with 38 additions and 43 deletions

View File

@@ -70,9 +70,10 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
try: try:
if name == "run_powershell": if name == "run_powershell":
script = arguments.get("script", "") script = arguments.get("script", "")
result = shell_runner.run_powershell(script, os.getcwd()) # run_powershell is synchronous, so we run it in a thread to avoid blocking the loop
result = await asyncio.to_thread(shell_runner.run_powershell, script, os.getcwd())
else: else:
result = mcp_client.dispatch(name, arguments) result = await mcp_client.async_dispatch(name, arguments)
return [TextContent(type="text", text=str(result))] return [TextContent(type="text", text=str(result))]
except Exception as e: except Exception as e:
return [TextContent(type="text", text=f"ERROR: {e}")] return [TextContent(type="text", text=f"ERROR: {e}")]

View File

@@ -859,88 +859,82 @@ def get_ui_performance() -> str:
TOOL_NAMES: set[str] = {"read_file", "list_directory", "search_files", "get_file_summary", "py_get_skeleton", "py_get_code_outline", "py_get_definition", "get_git_diff", "web_search", "fetch_url", "get_ui_performance", "get_file_slice", "set_file_slice", "edit_file", "py_update_definition", "py_get_signature", "py_set_signature", "py_get_class_summary", "py_get_var_declaration", "py_set_var_declaration", "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy", "py_get_docstring", "get_tree"} TOOL_NAMES: set[str] = {"read_file", "list_directory", "search_files", "get_file_summary", "py_get_skeleton", "py_get_code_outline", "py_get_definition", "get_git_diff", "web_search", "fetch_url", "get_ui_performance", "get_file_slice", "set_file_slice", "edit_file", "py_update_definition", "py_get_signature", "py_set_signature", "py_get_class_summary", "py_get_var_declaration", "py_set_var_declaration", "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy", "py_get_docstring", "get_tree"}
async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str: def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
""" """
Dispatch an MCP tool call by name asynchronously. Returns the result as a string. Dispatch an MCP tool call by name. Returns the result as a string.
""" """
# Handle aliases # Handle aliases
path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", "")))) path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))
if tool_name == "read_file": if tool_name == "read_file":
return await asyncio.to_thread(read_file, path) return read_file(path)
if tool_name == "list_directory": if tool_name == "list_directory":
return await asyncio.to_thread(list_directory, path) return list_directory(path)
if tool_name == "search_files": if tool_name == "search_files":
return await asyncio.to_thread(search_files, path, str(tool_input.get("pattern", "*"))) return search_files(path, str(tool_input.get("pattern", "*")))
if tool_name == "get_file_summary": if tool_name == "get_file_summary":
return await asyncio.to_thread(get_file_summary, path) return get_file_summary(path)
if tool_name == "py_get_skeleton": if tool_name == "py_get_skeleton":
return await asyncio.to_thread(py_get_skeleton, path) return py_get_skeleton(path)
if tool_name == "py_get_code_outline": if tool_name == "py_get_code_outline":
return await asyncio.to_thread(py_get_code_outline, path) return py_get_code_outline(path)
if tool_name == "py_get_definition": if tool_name == "py_get_definition":
return await asyncio.to_thread(py_get_definition, path, str(tool_input.get("name", ""))) return py_get_definition(path, str(tool_input.get("name", "")))
if tool_name == "py_update_definition": if tool_name == "py_update_definition":
return await asyncio.to_thread(py_update_definition, path, str(tool_input.get("name", "")), str(tool_input.get("new_content", ""))) return py_update_definition(path, str(tool_input.get("name", "")), str(tool_input.get("new_content", "")))
if tool_name == "py_get_signature": if tool_name == "py_get_signature":
return await asyncio.to_thread(py_get_signature, path, str(tool_input.get("name", ""))) return py_get_signature(path, str(tool_input.get("name", "")))
if tool_name == "py_set_signature": if tool_name == "py_set_signature":
return await asyncio.to_thread(py_set_signature, path, str(tool_input.get("name", "")), str(tool_input.get("new_signature", ""))) return py_set_signature(path, str(tool_input.get("name", "")), str(tool_input.get("new_signature", "")))
if tool_name == "py_get_class_summary": if tool_name == "py_get_class_summary":
return await asyncio.to_thread(py_get_class_summary, path, str(tool_input.get("name", ""))) return py_get_class_summary(path, str(tool_input.get("name", "")))
if tool_name == "py_get_var_declaration": if tool_name == "py_get_var_declaration":
return await asyncio.to_thread(py_get_var_declaration, path, str(tool_input.get("name", ""))) return py_get_var_declaration(path, str(tool_input.get("name", "")))
if tool_name == "py_set_var_declaration": if tool_name == "py_set_var_declaration":
return await asyncio.to_thread(py_set_var_declaration, path, str(tool_input.get("name", "")), str(tool_input.get("new_declaration", ""))) return py_set_var_declaration(path, str(tool_input.get("name", "")), str(tool_input.get("new_declaration", "")))
if tool_name == "get_file_slice": if tool_name == "get_file_slice":
return await asyncio.to_thread(get_file_slice, path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1))) return get_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)))
if tool_name == "set_file_slice": if tool_name == "set_file_slice":
return await asyncio.to_thread(set_file_slice, path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)), str(tool_input.get("new_content", ""))) return set_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)), str(tool_input.get("new_content", "")))
if tool_name == "get_git_diff": if tool_name == "get_git_diff":
return await asyncio.to_thread(get_git_diff, return get_git_diff(
path, path,
str(tool_input.get("base_rev", "HEAD")), str(tool_input.get("base_rev", "HEAD")),
str(tool_input.get("head_rev", "")) str(tool_input.get("head_rev", ""))
) )
if tool_name == "edit_file": if tool_name == "edit_file":
return await asyncio.to_thread(edit_file, return edit_file(
path, path,
str(tool_input.get("old_string", "")), str(tool_input.get("old_string", "")),
str(tool_input.get("new_string", "")), str(tool_input.get("new_string", "")),
bool(tool_input.get("replace_all", False)) bool(tool_input.get("replace_all", False))
) )
if tool_name == "web_search": if tool_name == "web_search":
return await asyncio.to_thread(web_search, str(tool_input.get("query", ""))) return web_search(str(tool_input.get("query", "")))
if tool_name == "fetch_url": if tool_name == "fetch_url":
return await asyncio.to_thread(fetch_url, str(tool_input.get("url", ""))) return fetch_url(str(tool_input.get("url", "")))
if tool_name == "get_ui_performance": if tool_name == "get_ui_performance":
return await asyncio.to_thread(get_ui_performance) return get_ui_performance()
if tool_name == "py_find_usages": if tool_name == "py_find_usages":
return await asyncio.to_thread(py_find_usages, path, str(tool_input.get("name", ""))) return py_find_usages(path, str(tool_input.get("name", "")))
if tool_name == "py_get_imports": if tool_name == "py_get_imports":
return await asyncio.to_thread(py_get_imports, path) return py_get_imports(path)
if tool_name == "py_check_syntax": if tool_name == "py_check_syntax":
return await asyncio.to_thread(py_check_syntax, path) return py_check_syntax(path)
if tool_name == "py_get_hierarchy": if tool_name == "py_get_hierarchy":
return await asyncio.to_thread(py_get_hierarchy, path, str(tool_input.get("class_name", ""))) return py_get_hierarchy(path, str(tool_input.get("class_name", "")))
if tool_name == "py_get_docstring": if tool_name == "py_get_docstring":
return await asyncio.to_thread(py_get_docstring, path, str(tool_input.get("name", ""))) return py_get_docstring(path, str(tool_input.get("name", "")))
if tool_name == "get_tree": if tool_name == "get_tree":
return await asyncio.to_thread(get_tree, path, int(tool_input.get("max_depth", 2))) return get_tree(path, int(tool_input.get("max_depth", 2)))
return f"ERROR: unknown MCP tool '{tool_name}'" return f"ERROR: unknown MCP tool '{tool_name}'"
def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str: async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
""" """
Dispatch an MCP tool call by name. Returns the result as a string. Dispatch an MCP tool call by name asynchronously. Returns the result as a string.
""" """
try: # Run blocking I/O bound tools in a thread to allow parallel execution via asyncio.gather
loop = asyncio.get_running_loop() return await asyncio.to_thread(dispatch, tool_name, tool_input)
# If we are in a running loop, we can't use asyncio.run
# But we are in a synchronous function.
# This is tricky. If we are in a thread, we might not have a loop.
return asyncio.run_coroutine_threadsafe(async_dispatch(tool_name, tool_input), loop).result()
except RuntimeError:
# No running loop, use asyncio.run
return asyncio.run(async_dispatch(tool_name, tool_input))
def get_tool_schemas() -> list[dict[str, Any]]: def get_tool_schemas() -> list[dict[str, Any]]: