Files
EvoScientist-Multi/EvoScientist/mcp
houren Antony 932c934485 fix(mcp): give stdio subprocess a real stderr fd under redirected streams (#423)
* fix(mcp): give stdio subprocess a real stderr fd under redirected streams (#418)

On Windows the Textual TUI redirects sys.stderr to an in-memory capture
(textual.app._PrintCapture) whose fileno() returns -1. The MCP SDK forwards
that stderr to stdio server subprocesses via subprocess.Popen(stderr=...),
and Popen rejects the invalid handle with OSError: [Errno 9] Bad file
descriptor — so only stdio servers fail to load (HTTP/SSE are unaffected).

Wrap mcp.client.stdio.stdio_client so that, whenever the configured errlog
has no usable fileno, it falls back to sys.__stderr__ (or os.devnull in GUI
hosts). Idempotent, no-op when the SDK is absent, warns if the SDK renames
stdio_client. Adds 9 regression tests and a troubleshooting note.

* fix(mcp): validate live fd and close fallback errlog after stdio session

Address CodeRabbit review on #423:
- _stdio_errlog_is_usable now os.fstat()s the fd to reject closed streams
  that still report their former positive fileno (prevents a deferred
  [Errno 9] from subprocess.Popen).
- The stdio_client wrapper owns the devnull fallback it allocates and
  closes it once the session exits, so repeated MCP reloads no longer leak
  file descriptors. Caller-provided usable errlogs pass through untouched.
- Tests cover the closed-fd case, the fd-leak/closure invariant, and
  confirm langchain-mcp-adapters binds the patched stdio_client.

* fix(mcp): rebind adapter stdio_client, forward errlog by kw, harden tests

Address CodeRabbit round-2 review on #423:
- The patch now also rebinds langchain_mcp_adapters.sessions.stdio_client,
  which the adapter captures via a 'from' import at module load — so the
  wrapped function reaches the adapter regardless of import order.
- errlog is forwarded to the SDK by keyword (original(server, *args,
  errlog=errlog, **kwargs)) so a future SDK inserting a positional
  parameter before errlog can't mis-bind the fallback.
- The fallback stream is now allocated inside the async context manager,
  so it is closed on session exit even if the CM is constructed but never
  entered (narrower fd-leak path).
- test_closed_fd_rejected now reaches the os.fstat branch (stale positive
  fd stub) instead of the ValueError path; test_adapter_binds_patched_stdio_client
  documents and asserts the import-order-independent rebind.

* fix(mcp): close fallback errlog when stdio_client construction fails

Address CodeRabbit round-3 review on #423: move the original(server, *args,
errlog=errlog, **kwargs) construction inside the try block so a failure
during subprocess/client setup still reaches the finally and closes the
wrapper-owned os.devnull stream. Added test_fallback_closed_when_construction_fails
covering the path.

* refactor(mcp): track fallback ownership via (stream, opened_by_us)

Address din0s review on #423:
- _safe_stdio_errlog() now returns (stream, opened_by_us); the wrapper closes
  the fallback only when opened_by_us is True, instead of inferring ownership
  from needs_fallback + an identity check against sys.__stderr__. Simpler and
  less likely to regress.
- Removed dead try/finally in test_closed_fd_rejected.
- Added test_wrapped_stdio_client_swaps_explicit_bad_errlog covering the
  'not _stdio_errlog_is_usable(errlog)' branch (explicit bad errlog, not the
  default sentinel).
- Updated test_safe_errlog_returns_usable_stream for the tuple return.

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
2026-08-13 17:39:45 +00:00
..

Model Context Protocol Integration

Connects external systems to EvoScientist via Model Context Protocol (MCP).

Tip

Explore more servers: MCP Server Directory

📖 Contents

🔌 Quick Start

Option A: Terminal — before or outside an agent session

EvoSci mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking

Option B: In-session — inside a running agent session

/mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking

Note

After adding a new server in-session, reload the agent session (/new in interactive mode) or restart the CLI agent to load the new config.

Option C: Edit YAML directly - advanced customization

# ~/.config/evoscientist/mcp.yaml
sequential-thinking:
  transport: stdio
  command: npx
  args: ["-y", "@modelcontextprotocol/server-sequential-thinking"]

🛠️ Command Reference

Syntax

EvoSci mcp add <name> <command-or-url> [args...] \
  [--transport <transport>] \
  [--tools <tool1,tool2,...>] \
  [--expose-to <agent1,agent2,...>] \
  [--header <Key:Value>]... \
  [--env <KEY=VALUE>]... \
  [--env-ref <KEY>]...

Options:

Flag Short Description
--transport Transport type (auto-inferred if omitted)
--tools -t Comma-separated tool allowlist, supports glob wildcards (omit = all tools)
--expose-to -e Comma-separated target agents (default: main)
--header -H HTTP header as Key:Value (repeatable)
--env Env var as KEY=VALUE for stdio subprocess (repeatable)
--env-ref Reference an existing env var by name (repeatable)

Note

  • --transport is auto-inferred from target: http(s) URL → http, otherwise stdio.
  • -- is recommended before server args that start with -, so they are passed to the MCP server command.

Management

Command Description
EvoSci mcp List configured servers
EvoSci mcp list List configured servers
EvoSci mcp config Show detailed config for all servers
EvoSci mcp config <name> Show detailed config for one server
EvoSci mcp add ... Add a server
EvoSci mcp edit ... Edit an existing server
EvoSci mcp remove <name> Remove a server

Tip

All commands also work as interactive slash commands: /mcp, /mcp list, /mcp config, /mcp add ..., /mcp edit ..., /mcp remove <name>.

Examples

# Local stdio server
EvoSci mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking

# Remote HTTP server (transport auto-detected)
EvoSci mcp add docs-langchain https://docs.langchain.com/mcp

# SSE endpoint (explicit transport override)
EvoSci mcp add research-sse https://example.com/sse --transport sse

# With tool routing to sub-agent + env var reference
EvoSci mcp add brave-search npx --env-ref BRAVE_API_KEY -e research-agent -- -y @modelcontextprotocol/server-brave-search

# With tool allowlist (glob wildcards)
EvoSci mcp add fs npx -t "read_*,write_*" -- -y @modelcontextprotocol/server-filesystem /workspace

# Context7 routed to multiple agents
EvoSci mcp add context7 npx -e main,research-agent,code-agent -- -y @upstash/context7-mcp@latest
More examples
# stdio transport (auto-detected from command)
EvoSci mcp add filesystem npx -- -y @modelcontextprotocol/server-filesystem /tmp

# http transport (auto-detected from URL)
EvoSci mcp add brave-search http://localhost:8080/mcp -H "Authorization:Bearer ${BRAVE_API_KEY}"

# sse transport, routed to a specific agent
EvoSci mcp add my-sse http://localhost:9090/sse --transport sse -e research-agent

# With tool allowlist (supports glob wildcards)
EvoSci mcp add fs npx -- -y @modelcontextprotocol/server-filesystem /tmp -t "read_*,write_*"

Editing Servers

Update individual fields without re-adding:

# Change routing
EvoSci mcp edit filesystem --expose-to main,code-agent

# Set a tool allowlist (supports glob wildcards)
EvoSci mcp edit filesystem --tools "read_*,write_*"

# Clear a tool allowlist (pass all tools)
EvoSci mcp edit filesystem --tools none

# Change URL
EvoSci mcp edit my-api --url http://new-host:9090/mcp

🔀 Tool Routing

Use expose_to to control which agents receive each server's tools:

postgres:
  transport: stdio
  command: npx
  args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
  expose_to: [data-analysis-agent]

github:
  transport: stdio
  command: npx
  args: ["-y", "@modelcontextprotocol/server-github"]
  expose_to: [main, research-agent]

Available agents:

Agent Role
main Main orchestrator (default target)
planner-agent Experiment planning
research-agent Literature search
code-agent Code writing
debug-agent Debugging
data-analysis-agent Data analysis
writing-agent Report writing

Note

Tools routed to sub-agents are injected automatically — no need to edit any EvoScientist/subagents/*.yaml file.

🔍 Tool Filtering with Wildcards

Use the tools field to filter which tools from a server are exposed. Supports glob-style wildcards:

# Exact matching (original behavior)
exa:
  transport: http
  url: https://mcp.exa.ai/mcp
  tools:
    - web_search_exa
    - get_code_context_exa
    - company_research_exa

# Wildcard: all tools ending with _exa
exa:
  transport: http
  url: https://mcp.exa.ai/mcp
  tools:
    - "*_exa"

# Multiple patterns
filesystem:
  transport: stdio
  command: npx
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
  tools:
    - "read_*"
    - "write_*"
    - "list_*"

# Mix wildcards and exact matches
mixed:
  transport: http
  url: https://example.com/mcp
  tools:
    - "search_*"
    - "get_metadata"

Wildcard Patterns

Pattern Matches Example
* Any sequence of characters *_exa matches web_search_exa, get_code_context_exa
? Any single character tool_? matches tool_1, tool_2 but not tool_10
[seq] Any character in sequence tool_[abc] matches tool_a, tool_b, tool_c
[0-9] Any character in range version_[0-9] matches version_0 through version_9
[!seq] Any character NOT in sequence tool_[!0-9] matches tool_a but not tool_1

📄 Config File

Path: ~/.config/evoscientist/mcp.yaml (or $XDG_CONFIG_HOME/evoscientist/mcp.yaml)

Config Fields

Field Required Description
transport Yes stdio, http, streamable_http, sse, websocket
command stdio only Command to run (e.g. npx)
args stdio only Arguments list
env No Environment variables for subprocess
url http/sse/ws Server URL
headers No HTTP headers (e.g. auth tokens)
tools No Tool allowlist with glob wildcards (omit = all tools)
expose_to No Target agents (default: ["main"])

Tip

Use ${VAR} in YAML values to reference environment variables. Missing variables are replaced with empty string and logged as a warning.

Supported Transports

Transport Config Fields
stdio command, args, env (optional)
http url, headers (optional)
streamable_http url, headers (optional)
sse url, headers (optional)
websocket url
Full YAML example with inline annotations
filesystem:
  transport: stdio
  command: npx
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
  tools: ["read_*", "write_*"]       # optional allowlist with wildcards (omit = all tools)
  expose_to: [main, code-agent]      # optional routing (omit = ["main"])

brave-search:
  transport: http
  url: "http://localhost:8080/mcp"
  headers:
    Authorization: "Bearer ${BRAVE_API_KEY}"
  expose_to: [research-agent]

⚙️ How It Works

  1. On agent startup (/new or create_cli_agent()), reads ~/.config/evoscientist/mcp.yaml
  2. Connects to each server via the configured transport
  3. Retrieves available tools from each server
  4. Filters tools by tools allowlist (if set)
  5. Routes tools to target agents by expose_to
  6. Tools are injected into the agent's tool list automatically

Note

MCP servers that fail to connect are skipped with a warning — they don't block startup. Tools are cached by config signature to avoid redundant loading.

🔧 Troubleshooting

Dependency missing (langchain-mcp-adapters)

Startup warning about MCP adapter missing:

pip install langchain-mcp-adapters
npx not available

stdio server fails to start — install Node.js and npx, or replace npx with a command available in your environment.

Windows: stdio server fails with [Errno 9] Bad file descriptor

On Windows, the TUI redirects sys.stderr to an in-memory capture whose fileno() is not a real OS handle. The MCP SDK forwards that stderr to the stdio server subprocess, and subprocess.Popen rejects the invalid handle with OSError: [Errno 9] Bad file descriptor — so only stdio servers fail to load (HTTP/SSE servers are unaffected).

EvoScientist wraps the SDK's stdio client so that, whenever the configured stderr has no usable file descriptor, it falls back to the original console handle (sys.__stderr__, or os.devnull in GUI hosts). If you still see this error, run from a real console (not pythonw.exe) and check the server's own startup output.

--env-ref or ${VAR} not resolving

Auth header/env becomes empty — ensure the variable exists in your environment before launching EvoScientist. ${VAR} interpolation happens at runtime; missing vars are replaced with empty strings and logged as warnings.

Server connects but no tools appear

Server exists in config but agent doesn't use MCP tools:

  • Confirm endpoint/command is healthy outside EvoScientist
  • Check auth headers/env
  • Check whether tools filter is too strict
Tool routing not taking effect

Tool is loaded but not available where expected:

  • Verify expose_to target names are correct (see Available agents above)
  • Reload the session with /new after config changes
  • Include main in expose_to if you need tools in the main agent

🔒 Security

Warning

Do not hardcode secrets in mcp.yaml. Use ${VAR} in YAML or --env-ref KEY in CLI to reference environment variables.

  • For filesystem-style servers, expose only minimum required paths
  • Use tools allowlists to limit which tools are loaded
  • Use expose_to to restrict which agents can access each server
  • Prefer least privilege for both tool scope and agent access