feat: implement _resolve_command function for command path resolution
This commit is contained in:
@@ -11,6 +11,8 @@ import fnmatch
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -492,6 +494,24 @@ def load_mcp_config() -> dict[str, Any]:
|
||||
return _interpolate_value(data)
|
||||
|
||||
|
||||
def _resolve_command(command: str) -> str:
|
||||
"""Resolve a stdio command to its full path.
|
||||
|
||||
Checks PATH first, then the current Python environment's bin directory
|
||||
(handles conda/venv envs where newly installed binaries may not be on PATH).
|
||||
Returns the original command string if not found (let the OS report the error).
|
||||
"""
|
||||
if os.path.isabs(command):
|
||||
return command
|
||||
found = shutil.which(command)
|
||||
if found:
|
||||
return found
|
||||
candidate = os.path.join(os.path.dirname(sys.executable), command)
|
||||
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
||||
return candidate
|
||||
return command
|
||||
|
||||
|
||||
def _build_connections(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""Convert YAML config to ``MultiServerMCPClient`` connections format.
|
||||
|
||||
@@ -505,7 +525,7 @@ def _build_connections(config: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
if transport == "stdio":
|
||||
conn: dict[str, Any] = {
|
||||
"transport": "stdio",
|
||||
"command": server.get("command", ""),
|
||||
"command": _resolve_command(server.get("command", "")),
|
||||
"args": server.get("args", []),
|
||||
}
|
||||
if "env" in server:
|
||||
|
||||
@@ -10,6 +10,7 @@ from EvoScientist.mcp.client import (
|
||||
_interpolate_env,
|
||||
_filter_tools,
|
||||
_route_tools,
|
||||
_resolve_command,
|
||||
_build_connections,
|
||||
load_mcp_config,
|
||||
add_mcp_server,
|
||||
@@ -99,6 +100,54 @@ class TestLoadMcpConfig:
|
||||
# ---- _build_connections ----
|
||||
|
||||
|
||||
# ---- _resolve_command ----
|
||||
|
||||
|
||||
class TestResolveCommand:
|
||||
def test_absolute_path_returned_as_is(self, tmp_path):
|
||||
"""Absolute paths are never modified, even if the file doesn't exist."""
|
||||
fake = str(tmp_path / "mytool")
|
||||
assert _resolve_command(fake) == fake
|
||||
|
||||
def test_found_on_path(self):
|
||||
"""Commands found via shutil.which are returned as full paths."""
|
||||
result = _resolve_command("python")
|
||||
assert result.endswith("python") or result.endswith("python3")
|
||||
assert result != "python" # resolved, not the bare name
|
||||
|
||||
def test_found_in_python_bin(self, tmp_path, monkeypatch):
|
||||
"""Falls back to sys.executable's directory when not in PATH."""
|
||||
|
||||
# Create a fake executable next to sys.executable
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
fake_exe = bin_dir / "my-mcp-tool"
|
||||
fake_exe.write_text("#!/bin/sh\n")
|
||||
fake_exe.chmod(0o755)
|
||||
|
||||
monkeypatch.setattr("shutil.which", lambda _: None)
|
||||
monkeypatch.setattr("EvoScientist.mcp.client.sys.executable", str(bin_dir / "python"))
|
||||
|
||||
assert _resolve_command("my-mcp-tool") == str(fake_exe)
|
||||
|
||||
def test_not_found_returns_original(self, monkeypatch):
|
||||
"""Returns the original command when not found anywhere (let OS report the error)."""
|
||||
monkeypatch.setattr("shutil.which", lambda _: None)
|
||||
monkeypatch.setattr(
|
||||
"EvoScientist.mcp.client.sys.executable", "/nonexistent/bin/python"
|
||||
)
|
||||
assert _resolve_command("unknown-tool-xyz") == "unknown-tool-xyz"
|
||||
|
||||
def test_build_connections_resolves_command(self, monkeypatch):
|
||||
"""_build_connections uses _resolve_command so the full path appears in output."""
|
||||
monkeypatch.setattr(
|
||||
"EvoScientist.mcp.client._resolve_command", lambda cmd: f"/resolved/{cmd}"
|
||||
)
|
||||
config = {"srv": {"transport": "stdio", "command": "mytool", "args": []}}
|
||||
conns = _build_connections(config)
|
||||
assert conns["srv"]["command"] == "/resolved/mytool"
|
||||
|
||||
|
||||
class TestBuildConnections:
|
||||
def test_stdio_connection(self):
|
||||
config = {
|
||||
@@ -111,7 +160,7 @@ class TestBuildConnections:
|
||||
conns = _build_connections(config)
|
||||
assert "fs" in conns
|
||||
assert conns["fs"]["transport"] == "stdio"
|
||||
assert conns["fs"]["command"] == "npx"
|
||||
assert conns["fs"]["command"].endswith("npx")
|
||||
assert conns["fs"]["args"] == ["-y", "server"]
|
||||
|
||||
def test_stdio_with_env(self):
|
||||
|
||||
Reference in New Issue
Block a user