Add MCP docs and CLI polish: docs page, startup connect summary, --mcp-config flag, compact tool output

This commit is contained in:
Jonathan Singer
2026-08-20 15:39:50 -04:00
parent 209584e7fd
commit 8fb83f52b1
13 changed files with 286 additions and 137 deletions
+3 -3
View File
@@ -333,11 +333,11 @@ Strix can connect to Model Context Protocol (MCP) servers you list and expose th
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
},
{
"name": "vercel",
"name": "github",
"transport": "http",
"url": "https://mcp.vercel.com",
"url": "https://api.githubcopilot.com/mcp/",
"auth": { "kind": "bearer", "token": "your-token" },
"allowed_tools": ["list_projects"]
"allowed_tools": ["list_issues"]
}
]
```
+2 -1
View File
@@ -47,7 +47,8 @@
"pages": [
"integrations/github-actions",
"integrations/ci-cd",
"integrations/coding-agents"
"integrations/coding-agents",
"integrations/mcp"
]
},
{
+92
View File
@@ -0,0 +1,92 @@
---
title: "MCP Servers"
description: "Connect your own MCP servers and expose their tools to the agent"
---
Strix can connect to [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers you list and expose their tools to the agent during a run. Use this to give the agent extra capabilities — reading files, querying an issue tracker, or any other tool a server offers.
## Setup
Create the file `~/.strix/mcp-servers.json`. It holds a JSON list of the servers you want the agent to reach. Each entry is either a local `stdio` server that Strix launches as a subprocess, or a remote `http` server.
Create the directory if it does not exist, then write the file:
```bash
mkdir -p ~/.strix
```
Paste the servers you want into `~/.strix/mcp-servers.json`. The example below shows one of each transport — a local filesystem server over `stdio` and a remote GitHub server over `http` with a bearer token:
```json
[
{
"name": "local_fs",
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
},
{
"name": "github",
"transport": "http",
"url": "https://api.githubcopilot.com/mcp/",
"auth": { "kind": "bearer", "token": "your-token" },
"allowed_tools": ["list_issues"]
}
]
```
Strix reads this file at the start of each run. There is no default file, so no MCP tools are loaded until you create it. Edit `command`, `args`, `url`, and `token` to match your own servers.
## Fields
<ParamField path="name" type="string" required>
A short label for the connection. Each server's tools are namespaced by
`name` (for example `local_fs.read_file`), so two servers can offer the same
tool name without colliding.
</ParamField>
<ParamField path="transport" type="string">
`stdio` for a local subprocess server, or `http` for a remote server.
</ParamField>
<ParamField path="command" type="string">
For `stdio` servers: the executable Strix launches (for example `npx`).
</ParamField>
<ParamField path="args" type="array">
For `stdio` servers: the arguments passed to `command`.
</ParamField>
<ParamField path="url" type="string">
For `http` servers: the server endpoint URL.
</ParamField>
<ParamField path="auth" type="object">
For `http` servers that need a bearer token:
`{ "kind": "bearer", "token": "your-token" }`.
</ParamField>
<ParamField path="allowed_tools" type="array">
Restrict which tools the agent can call. Omit it to expose every tool the
server offers, or set it to a list of tool names to allow only those.
</ParamField>
## Pointing at a different file
To read the config from another path instead of `~/.strix/mcp-servers.json`, either pass `--mcp-config <path>` on the command line:
```bash
strix --mcp-config ./mcp-servers.json -t ...
```
or set the `STRIX_MCP_CONFIG` environment variable to that path. The flag takes precedence when both are given.
## Startup confirmation
When servers are configured, Strix prints a one-line summary at scan startup, for example `MCP: connected 1 server (14 tools): local_fs`, so you can confirm your servers connected.
## Behavior
- The config file is optional. Without it, a run simply gets no MCP tools.
- A server that fails to connect is skipped and logged, and the run continues without it.
- A single malformed entry is skipped without blocking the valid ones.
+17 -4
View File
@@ -56,6 +56,7 @@ if TYPE_CHECKING:
from agents.result import RunResultBase
from strix.runtime.status import StatusSink
from strix.tools.mcp import ConnectedMcpServer
logger = logging.getLogger(__name__)
@@ -63,6 +64,16 @@ logger = logging.getLogger(__name__)
StreamEventSink = Callable[[str, Any], None]
def _mcp_startup_summary(connections: list[ConnectedMcpServer]) -> str:
"""One user-facing line summarizing the MCP servers that connected."""
server_count = len(connections)
tool_count = sum(c.tool_count for c in connections)
servers_word = "server" if server_count == 1 else "servers"
tools_word = "tool" if tool_count == 1 else "tools"
names = ", ".join(c.name for c in connections)
return f"MCP: connected {server_count} {servers_word} ({tool_count} {tools_word}): {names}"
def _merge_root_prompt_context(
scope_context: dict[str, Any],
extra_system_prompt_context: dict[str, Any] | None,
@@ -301,15 +312,17 @@ async def run_strix_scan(
)
# Connect any MCP servers the user listed in ~/.strix/mcp-servers.json and
# register their tools before the agent is built. These are the user's own
# servers, so no result scrub is applied. Fully fail-open: a missing file or
# a failed connection must never break a normal run (a managed run has no file).
# register their tools before the agent is built. Fail-open: a missing
# config, or a server that will not connect, must never break a run.
from strix.tools.mcp import connect_mcp_servers, load_user_mcp_configs
try:
user_mcp_configs = load_user_mcp_configs()
if user_mcp_configs:
mcp_servers = await connect_mcp_servers(user_mcp_configs)
connections = await connect_mcp_servers(user_mcp_configs)
mcp_servers = [c.server for c in connections]
if connections:
report(_mcp_startup_summary(connections))
except Exception:
logger.exception("Failed to connect user MCP servers; continuing without them")
+16
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
@@ -219,6 +220,13 @@ Examples:
help="Path to a custom config file (JSON) to use instead of ~/.strix/cli-config.json",
)
parser.add_argument(
"--mcp-config",
type=str,
metavar="PATH",
help="Path to an MCP servers JSON file to use instead of ~/.strix/mcp-servers.json.",
)
parser.add_argument(
"--max-budget",
"--max-budget-usd",
@@ -267,6 +275,14 @@ Examples:
if args.config:
apply_config_override(validate_config_file(args.config))
if args.mcp_config:
mcp_config_path = Path(args.mcp_config).expanduser()
if not mcp_config_path.is_file():
parser.error(f"--mcp-config file not found: {args.mcp_config}")
# The MCP loader reads this env var as its config-path override, so
# setting it here makes the flag win over the default location.
os.environ["STRIX_MCP_CONFIG"] = str(mcp_config_path)
if args.update:
sys.exit(0 if self_update() else 1)
@@ -22,19 +22,20 @@ func statusIcon(status string) (string, lipgloss.Style) {
return "○ Unknown", Dim()
}
// renderGenericTool ports registry._render_default_tool_widget.
func renderGenericTool(name string, args map[string]any, result any, status string) string {
// renderGenericTool ports registry._render_default_tool_widget. It shows the
// tool name, its arguments, and a status line only. The raw result is
// deliberately not rendered: a generic/MCP result (e.g. a multi-kilobyte JSON
// payload from a database query tool) is noise on screen, and the agent narrates
// what it got in its next message. The full result still lives in the event
// data, the run log, and the `strix view` viewer.
func renderGenericTool(name string, args map[string]any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render("→ Using tool ") + Bold(Blue).Render(name) + "\n")
for _, k := range SortedKeys(args) {
b.WriteString(" " + Dim().Render(k) + ": " + StringValue(args[k]) + "\n")
}
if (status == "completed" || status == "failed" || status == "error") && result != nil {
b.WriteString(lipgloss.NewStyle().Bold(true).Render("Result: ") + StringValue(result))
} else {
icon, style := statusIcon(status)
b.WriteString(style.Render(icon))
}
icon, style := statusIcon(status)
b.WriteString(style.Render(icon))
return b.String()
}
@@ -87,7 +88,7 @@ func Tool(data map[string]any) string {
case "list_requests", "view_request", "repeat_request", "list_sitemap", "view_sitemap_entry", "scope_rules":
return renderProxyTool(name, args, result, status)
}
return renderGenericTool(name, args, result, status)
return renderGenericTool(name, args, status)
}
// ---------------------------------------------------------------------------
@@ -203,7 +203,7 @@ func TestToolDispatchCoversKnownTools(t *testing.T) {
{
"unknown tool falls back to generic",
tool("brand_new_tool", map[string]any{"alpha": "1"}, "done", "completed"),
[]string{"brand_new_tool", "alpha", "Result:", "done"},
[]string{"brand_new_tool", "alpha", "Done"},
},
}
@@ -214,6 +214,18 @@ func TestToolDispatchCoversKnownTools(t *testing.T) {
}
}
func TestGenericToolOmitsRawResult(t *testing.T) {
// The generic/MCP renderer shows tool name, args, and a status line only —
// never the raw result payload.
long := strings.Repeat("x", 5000)
out := ansi.Strip(Tool(tool("db_query", map[string]any{"query": "select 1"}, long, "completed")))
requireContains(t, out, "db_query", "query", "Done")
if strings.Contains(out, "Result:") || strings.Contains(out, strings.Repeat("x", 20)) {
t.Fatalf("generic result body must not be rendered:\n%s", out)
}
}
func TestCollapseToolShellPreviewAndExpand(t *testing.T) {
lines := make([]string, 16)
for i := range lines {
+2 -3
View File
@@ -2,9 +2,8 @@
from __future__ import annotations
from strix.tools.mcp.client import connect_mcp_servers
from strix.tools.mcp.client import ConnectedMcpServer, connect_mcp_servers
from strix.tools.mcp.config import (
AwsSigV4Auth,
BearerAuth,
McpAuth,
McpConnectionConfig,
@@ -13,8 +12,8 @@ from strix.tools.mcp.loader import load_user_mcp_configs
__all__ = [
"AwsSigV4Auth",
"BearerAuth",
"ConnectedMcpServer",
"McpAuth",
"McpConnectionConfig",
"connect_mcp_servers",
+30 -21
View File
@@ -15,7 +15,7 @@ from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, NamedTuple, cast
from agents.exceptions import ModelBehaviorError
from agents.mcp import (
@@ -29,7 +29,6 @@ from agents.mcp import (
)
from strix.agents.factory import register_agent_tools
from strix.tools.mcp.config import BearerAuth, McpConnectionConfig
if TYPE_CHECKING:
@@ -38,6 +37,8 @@ if TYPE_CHECKING:
from agents.tool import FunctionTool, Tool
from mcp.types import Tool as MCPTool
from strix.tools.mcp.config import McpConnectionConfig
# Runs on each tool's structured result before it reaches the agent. Called
# ``result_transform(namespaced_tool_name, structured_result)`` and its return
# value becomes the tool's output. ``structured_result`` is the parsed
@@ -49,23 +50,24 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
class ConnectedMcpServer(NamedTuple):
"""One successfully connected MCP server and how many tools it registered.
``server`` is kept so the caller can clean it up when the run ends;
``name`` and ``tool_count`` let the caller show the user a startup summary.
"""
server: MCPServer
name: str
tool_count: int
def _auth_headers(config: McpConnectionConfig) -> dict[str, str]:
"""Build the per-server request headers from the connection's auth."""
auth = config.auth
if auth is None:
return {}
if isinstance(auth, BearerAuth):
return {"Authorization": f"Bearer {auth.token}"}
# The only other variant is AWS SigV4.
# TODO: AWS SigV4 transport and auth are UNVERIFIED. Confirm how the target
# AWS MCP server is reached (stdio vs streamable HTTP) and how it accepts
# SigV4-signed requests before enabling this branch. Do not fabricate request
# signing here.
raise NotImplementedError(
"AWS SigV4 MCP auth is not verified yet; confirm the server's transport "
"and request signing before connecting an aws_sigv4 connection."
)
return {"Authorization": f"Bearer {auth.token}"}
def _build_server(config: McpConnectionConfig) -> MCPServer:
@@ -209,7 +211,7 @@ async def _register_server_tools(
async def connect_mcp_servers(
configs: list[McpConnectionConfig],
result_transform: ResultTransform | None = None,
) -> list[MCPServer]:
) -> list[ConnectedMcpServer]:
"""Connect to each MCP server and register its tools.
When ``result_transform`` is given, every registered tool routes its result
@@ -217,21 +219,28 @@ async def connect_mcp_servers(
:func:`_install_result_transform`). When it is ``None`` the tools behave
exactly as the SDK builds them.
Returns the servers that connected, so the caller can clean them up when the
run ends. Connections that fail are skipped rather than raised.
Returns one :class:`ConnectedMcpServer` per server that connected, carrying
the SDK server (so the caller can clean it up when the run ends) plus the
server name and how many tools it registered (so the caller can show the
user a startup summary). Connections that fail are skipped rather than
raised.
"""
connected: list[MCPServer] = []
connected: list[ConnectedMcpServer] = []
for config in configs:
server = _build_server(config)
server: MCPServer | None = None
try:
server = _build_server(config)
await server.connect() # type: ignore[no-untyped-call]
tools = await _register_server_tools(config, server, result_transform)
except Exception:
logger.exception("Skipping MCP connection %r", config.name)
await server.cleanup() # type: ignore[no-untyped-call]
if server is not None:
await server.cleanup() # type: ignore[no-untyped-call]
continue
logger.info("Connected MCP server %r (%d tools)", config.name, len(tools))
connected.append(server)
connected.append(
ConnectedMcpServer(server=server, name=config.name, tool_count=len(tools))
)
return connected
+3 -20
View File
@@ -21,24 +21,7 @@ class BearerAuth(BaseModel):
token: str = Field(min_length=1, repr=False)
class AwsSigV4Auth(BaseModel):
"""Request-signing auth for AWS.
AWS does not authenticate with a header token; each request is signed. The
temporary key is minted per run and passed out of band, never read from the
ambient environment.
"""
model_config = ConfigDict(extra="forbid")
kind: Literal["aws_sigv4"] = "aws_sigv4"
access_key_id: str = Field(min_length=1, repr=False)
secret_access_key: str = Field(min_length=1, repr=False)
session_token: str | None = Field(default=None, repr=False)
region: str = Field(min_length=1)
McpAuth = Annotated[BearerAuth | AwsSigV4Auth, Field(discriminator="kind")]
McpAuth = Annotated[BearerAuth, Field(discriminator="kind")]
class McpConnectionConfig(BaseModel):
@@ -60,8 +43,8 @@ class McpConnectionConfig(BaseModel):
"""The MCP server endpoint. Required for ``http``."""
auth: McpAuth | None = None
"""Bearer token or AWS SigV4 signing material. Optional; a local stdio
server usually needs none."""
"""Bearer token for the server. Optional; a local stdio server usually
needs none."""
command: str | None = Field(default=None, min_length=1)
"""The executable to launch for ``stdio``. Required for ``stdio``."""
+2 -2
View File
@@ -2,8 +2,8 @@
An open-source user lists the MCP servers they want the agent to reach in a
small JSON file. Strix reads it at the start of a run, connects to each server,
and registers its tools. The file is optional: a managed/saas run simply won't
have one, which is fine.
and registers its tools. The file is optional; without it the run simply gets
no MCP tools.
Parsing is fail-open. A single malformed entry is logged and skipped rather than
raising, so one bad row never blocks the servers that are valid, and a missing
+61
View File
@@ -0,0 +1,61 @@
"""Tests for the --mcp-config CLI flag."""
from __future__ import annotations
import importlib
import os
import sys
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any
import pytest
if TYPE_CHECKING:
from pathlib import Path
cli_main: Any = importlib.import_module("strix.interface.main")
def _stub_settings(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
cli_main,
"load_settings",
lambda: SimpleNamespace(runtime=SimpleNamespace(max_local_copy_mb=1024)),
)
def test_mcp_config_flag_sets_loader_override(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
config = tmp_path / "servers.json"
config.write_text("[]", encoding="utf-8")
_stub_settings(monkeypatch)
# delenv records "originally absent" so monkeypatch removes whatever the
# parser sets, keeping the override from leaking into other tests.
monkeypatch.delenv("STRIX_MCP_CONFIG", raising=False)
monkeypatch.setattr(
sys, "argv", ["strix", "-t", "https://test.com/", "-n", "--mcp-config", str(config)]
)
args = cli_main.parse_arguments()
assert args.mcp_config == str(config)
assert os.environ["STRIX_MCP_CONFIG"] == str(config)
def test_mcp_config_flag_rejects_missing_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
_stub_settings(monkeypatch)
monkeypatch.delenv("STRIX_MCP_CONFIG", raising=False)
missing = tmp_path / "nope.json"
monkeypatch.setattr(
sys, "argv", ["strix", "-t", "https://test.com/", "-n", "--mcp-config", str(missing)]
)
with pytest.raises(SystemExit):
cli_main.parse_arguments()
assert "--mcp-config file not found" in capsys.readouterr().err
+35 -73
View File
@@ -13,7 +13,6 @@ from pydantic import ValidationError
from strix.agents import factory
from strix.tools.mcp import (
AwsSigV4Auth,
BearerAuth,
McpConnectionConfig,
load_user_mcp_configs,
@@ -102,39 +101,17 @@ def _reset_registry() -> Any:
def test_bearer_config_parses_from_dict() -> None:
config = McpConnectionConfig.model_validate(
{
"name": "vercel_main",
"name": "files_main",
"transport": "http",
"url": "https://mcp.example.com",
"auth": {"kind": "bearer", "token": "abc"},
"allowed_tools": ["list_projects"],
"allowed_tools": ["list_files"],
}
)
assert isinstance(config.auth, BearerAuth)
assert config.auth.token == "abc"
assert config.allowed_tools == ["list_projects"]
def test_aws_sigv4_config_parses_from_dict() -> None:
config = McpConnectionConfig.model_validate(
{
"name": "aws_production",
"url": "https://mcp.example.com",
"auth": {
"kind": "aws_sigv4",
"access_key_id": "AKIA",
"secret_access_key": "secret",
"session_token": "session",
"region": "us-east-1",
},
}
)
assert isinstance(config.auth, AwsSigV4Auth)
assert config.auth.region == "us-east-1"
# transport defaults to http, allowed_tools to None ("all tools").
assert config.transport == "http"
assert config.allowed_tools is None
assert config.allowed_tools == ["list_files"]
def test_unknown_auth_kind_is_rejected() -> None:
@@ -216,26 +193,11 @@ def test_unknown_field_is_rejected() -> None:
def test_bearer_auth_builds_authorization_header() -> None:
headers = _auth_headers(_config("vercel_main", []))
headers = _auth_headers(_config("files_main", []))
assert headers == {"Authorization": "Bearer run-token"}
def test_aws_sigv4_auth_is_not_implemented_yet() -> None:
config = McpConnectionConfig(
name="aws_production",
url="https://mcp.example.com",
auth=AwsSigV4Auth(
access_key_id="AKIA",
secret_access_key="secret",
region="us-east-1",
),
)
with pytest.raises(NotImplementedError):
_auth_headers(config)
# --- namespacing and filtering -----------------------------------------------
@@ -258,15 +220,15 @@ async def test_tools_are_namespaced_per_connection() -> None:
@pytest.mark.asyncio
async def test_disallowed_tool_is_not_registered() -> None:
server = FakeMCPServer(
"vercel_main",
[_mcp_tool("list_projects"), _mcp_tool("delete_project")],
"files_main",
[_mcp_tool("list_files"), _mcp_tool("search")],
)
await _register_server_tools(_config("vercel_main", ["list_projects"]), server)
await _register_server_tools(_config("files_main", ["list_files"]), server)
names = _registered_names()
assert "vercel_main.list_projects" in names
assert "vercel_main.delete_project" not in names
assert "files_main.list_files" in names
assert "files_main.search" not in names
@pytest.mark.asyncio
@@ -299,18 +261,18 @@ async def test_allowed_tools_list_restricts_registration() -> None:
@pytest.mark.asyncio
async def test_registered_tool_routes_to_its_server_with_the_original_name() -> None:
server = FakeMCPServer("vercel_main", [_mcp_tool("list_projects")])
server = FakeMCPServer("files_main", [_mcp_tool("list_files")])
tools: list[Tool] = await _register_server_tools(
_config("vercel_main", ["list_projects"]), server
_config("files_main", ["list_files"]), server
)
tool = tools[0]
output = await tool.on_invoke_tool(None, "{}") # type: ignore[union-attr]
# The call reaches the right server, addressed by the unprefixed remote name.
assert server.calls == [("list_projects", {})]
assert output == {"type": "text", "text": "routed:list_projects"}
assert server.calls == [("list_files", {})]
assert output == {"type": "text", "text": "routed:list_files"}
# --- result transform --------------------------------------------------------
@@ -318,64 +280,64 @@ async def test_registered_tool_routes_to_its_server_with_the_original_name() ->
@pytest.mark.asyncio
async def test_result_transform_receives_namespaced_name_and_structured_result() -> None:
server = FakeMCPServer("vercel_main", [_mcp_tool("list_projects")])
server = FakeMCPServer("files_main", [_mcp_tool("list_files")])
seen: list[tuple[str, Any]] = []
def transform(name: str, structured: Any) -> Any:
seen.append((name, structured))
return "scrubbed"
return {"kept": structured["content"][0]["text"]}
tools: list[Tool] = await _register_server_tools(
_config("vercel_main", ["list_projects"]), server, result_transform=transform
_config("files_main", ["list_files"]), server, result_transform=transform
)
output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr]
# The underlying MCP call still routes by the unprefixed remote name.
assert server.calls == [("list_projects", {})]
assert server.calls == [("list_files", {})]
# The transform is called with the namespaced name and the parsed result.
assert len(seen) == 1
name, structured = seen[0]
assert name == "vercel_main.list_projects"
assert name == "files_main.list_files"
# A parsed CallToolResult (dict/list), not a pre-serialized string.
assert structured["content"][0]["text"] == "routed:list_projects"
assert structured["content"][0]["text"] == "routed:list_files"
assert structured["isError"] is False
# The transform's return value is exactly what the tool yields.
assert output == "scrubbed"
assert output == {"kept": "routed:list_files"}
@pytest.mark.asyncio
async def test_result_transform_can_rewrite_the_tool_output() -> None:
server = FakeMCPServer("vercel_main", [_mcp_tool("list_projects")])
server = FakeMCPServer("files_main", [_mcp_tool("list_files")])
def transform(_name: str, structured: Any) -> Any:
# Withhold everything but a redacted view of the text field.
return f"redacted<{structured['content'][0]['text']}>"
# Keep only a truncated view of the text field.
return structured["content"][0]["text"][:6]
tools: list[Tool] = await _register_server_tools(
_config("vercel_main", ["list_projects"]), server, result_transform=transform
_config("files_main", ["list_files"]), server, result_transform=transform
)
output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr]
assert output == "redacted<routed:list_projects>"
assert output == "routed"
@pytest.mark.asyncio
async def test_without_result_transform_output_is_unchanged() -> None:
server = FakeMCPServer("vercel_main", [_mcp_tool("list_projects")])
server = FakeMCPServer("files_main", [_mcp_tool("list_files")])
tools: list[Tool] = await _register_server_tools(
_config("vercel_main", ["list_projects"]), server, result_transform=None
_config("files_main", ["list_files"]), server, result_transform=None
)
output = await tools[0].on_invoke_tool(None, "{}") # type: ignore[union-attr]
# Same shape the SDK produces today: no transform in the path.
assert server.calls == [("list_projects", {})]
assert output == {"type": "text", "text": "routed:list_projects"}
assert server.calls == [("list_files", {})]
assert output == {"type": "text", "text": "routed:list_files"}
# --- server build branch -----------------------------------------------------
@@ -401,10 +363,10 @@ def test_build_server_stdio_branch() -> None:
def test_build_server_http_branch() -> None:
server = _build_server(_config("vercel_main", ["list_projects"]))
server = _build_server(_config("files_main", ["list_files"]))
assert isinstance(server, MCPServerStreamableHttp)
assert server.name == "vercel_main"
assert server.name == "files_main"
# --- loader ------------------------------------------------------------------
@@ -422,11 +384,11 @@ def test_loader_parses_stdio_and_http_entries(tmp_path: Path) -> None:
"args": ["-y", "server-filesystem"],
},
{
"name": "vercel_main",
"name": "files_main",
"transport": "http",
"url": "https://mcp.example.com",
"auth": {"kind": "bearer", "token": "abc"},
"allowed_tools": ["list_projects"],
"allowed_tools": ["list_files"],
},
]
),
@@ -435,9 +397,9 @@ def test_loader_parses_stdio_and_http_entries(tmp_path: Path) -> None:
configs = load_user_mcp_configs(config_file)
assert [c.name for c in configs] == ["local_fs", "vercel_main"]
assert [c.name for c in configs] == ["local_fs", "files_main"]
assert configs[0].transport == "stdio"
assert configs[1].allowed_tools == ["list_projects"]
assert configs[1].allowed_tools == ["list_files"]
def test_loader_skips_bad_entry_but_keeps_good_ones(tmp_path: Path) -> None: