Files
EvoScientist-Multi/EvoScientist/mcp/README.md
T
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

367 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Model Context Protocol Integration
> Connects external systems to [EvoScientist](https://github.com/EvoScientist/EvoScientist) via [Model Context Protocol (MCP)](https://modelcontextprotocol.io/).
> [!TIP]
> Explore more servers: [MCP Server Directory](https://github.com/modelcontextprotocol/servers)
## 📖 Contents
- [🔌 Quick Start](#-quick-start)
- [🛠️ Command Reference](#️-command-reference)
- [Syntax](#syntax)
- [Management](#management)
- [Examples](#examples)
- [Editing Servers](#editing-servers)
- [🔀 Tool Routing](#-tool-routing)
- [🔍 Tool Filtering with Wildcards](#-tool-filtering-with-wildcards)
- [Wildcard Patterns](#wildcard-patterns)
- [📄 Config File](#-config-file)
- [Config Fields](#config-fields)
- [Supported Transports](#supported-transports)
- [⚙️ How It Works](#️-how-it-works)
- [🔧 Troubleshooting](#-troubleshooting)
- [🔒 Security](#-security)
## 🔌 Quick Start
### Option A: Terminal — before or outside an agent session
```bash
EvoSci mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking
```
### Option B: In-session — inside a running agent session
```bash
/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
```yaml
# ~/.config/evoscientist/mcp.yaml
sequential-thinking:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"]
```
## 🛠️ Command Reference
### Syntax
```bash
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
```bash
# 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
```
<details>
<summary><strong>More examples</strong></summary>
```bash
# 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_*"
```
</details>
### Editing Servers
Update individual fields without re-adding:
```bash
# 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:
```yaml
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:
```yaml
# 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` |
<details>
<summary><strong>Full YAML example with inline annotations</strong></summary>
```yaml
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]
```
</details>
## ⚙️ 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
<details open>
<summary><strong>Dependency missing (<code>langchain-mcp-adapters</code>)</strong></summary>
Startup warning about MCP adapter missing:
```bash
pip install langchain-mcp-adapters
```
</details>
<details>
<summary><strong><code>npx</code> not available</strong></summary>
stdio server fails to start — install Node.js and `npx`, or replace `npx` with a command available in your environment.
</details>
<details>
<summary><strong>Windows: stdio server fails with <code>[Errno 9] Bad file descriptor</code></strong></summary>
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.
</details>
<details>
<summary><strong><code>--env-ref</code> or <code>${VAR}</code> not resolving</strong></summary>
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.
</details>
<details>
<summary><strong>Server connects but no tools appear</strong></summary>
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
</details>
<details>
<summary><strong>Tool routing not taking effect</strong></summary>
Tool is loaded but not available where expected:
- Verify `expose_to` target names are correct (see [Available agents](#-tool-routing) above)
- Reload the session with `/new` after config changes
- Include `main` in `expose_to` if you need tools in the main agent
</details>
## 🔒 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