update
This commit is contained in:
+186
-19
@@ -272,7 +272,7 @@ def _cmd_install_skill(source: str) -> None:
|
||||
console.print(f"[dim]Description:[/dim] {result.get('description', '(none)')}")
|
||||
console.print(f"[dim]Path:[/dim] [cyan]{_shorten_path(result['path'])}[/cyan]")
|
||||
console.print()
|
||||
console.print("[dim]Reload the agent with /new to use the skill.[/dim]")
|
||||
console.print("[dim]Reload with /new to apply.[/dim]")
|
||||
else:
|
||||
console.print(f"[red]Failed:[/red] {result['error']}")
|
||||
console.print()
|
||||
@@ -292,7 +292,7 @@ def _cmd_uninstall_skill(name: str) -> None:
|
||||
|
||||
if result["success"]:
|
||||
console.print(f"[green]Uninstalled:[/green] {name}")
|
||||
console.print("[dim]Reload the agent with /new to apply changes.[/dim]")
|
||||
console.print("[dim]Reload with /new to apply.[/dim]")
|
||||
else:
|
||||
console.print(f"[red]Failed:[/red] {result['error']}")
|
||||
console.print()
|
||||
@@ -388,6 +388,7 @@ def _cmd_channel(args: str, agent: Any, thread_id: str) -> None:
|
||||
def _mcp_list_servers() -> None:
|
||||
"""Print a table of configured MCP servers."""
|
||||
from .mcp import load_mcp_config
|
||||
from .mcp.client import USER_MCP_CONFIG
|
||||
|
||||
config = load_mcp_config()
|
||||
|
||||
@@ -414,7 +415,7 @@ def _mcp_list_servers() -> None:
|
||||
table.add_row(name, transport, tools_str, expose_str)
|
||||
|
||||
console.print(table)
|
||||
console.print("\n[dim]User config:[/dim] [cyan]~/.config/evoscientist/mcp.yaml[/cyan]")
|
||||
console.print(f"\n[dim]Config file: {USER_MCP_CONFIG}[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
@@ -424,20 +425,22 @@ def _cmd_mcp_add(args_str: str) -> None:
|
||||
from .mcp import add_mcp_server, parse_mcp_add_args
|
||||
|
||||
if not args_str.strip():
|
||||
console.print("[bold]Usage:[/bold] /mcp add <name> <transport> <command-or-url> [args...]")
|
||||
console.print("[bold]Usage:[/bold] /mcp add <name> <command-or-url> [args...]")
|
||||
console.print()
|
||||
console.print("[dim]Transports:[/dim] stdio, http, sse, websocket")
|
||||
console.print("[dim]Transport is auto-detected: URLs → http, commands → stdio[/dim]")
|
||||
console.print()
|
||||
console.print("[bold]Examples:[/bold]")
|
||||
console.print(" /mcp add filesystem stdio npx -y @modelcontextprotocol/server-filesystem /tmp")
|
||||
console.print(" /mcp add my-api http http://localhost:8080/mcp --header Authorization:Bearer\\ tok")
|
||||
console.print(" /mcp add my-api sse http://localhost:9090/sse --expose-to research-agent")
|
||||
console.print(" /mcp add sequential-thinking npx -y @modelcontextprotocol/server-sequential-thinking")
|
||||
console.print(" /mcp add docs-langchain https://docs.langchain.com/mcp")
|
||||
console.print(" /mcp add my-sse http://localhost:9090/sse --transport sse --expose-to research-agent")
|
||||
console.print()
|
||||
console.print("[dim]Options:[/dim]")
|
||||
console.print(" --transport T Transport type (default: auto-detect)")
|
||||
console.print(" --tools t1,t2 Tool allowlist")
|
||||
console.print(" --expose-to a1,a2 Target agents (default: main)")
|
||||
console.print(" --header Key:Value HTTP header (repeatable)")
|
||||
console.print(" --env KEY=VALUE Env var for stdio (repeatable)")
|
||||
console.print(" --env-ref KEY Env var as runtime ${KEY} reference (repeatable)")
|
||||
console.print()
|
||||
return
|
||||
|
||||
@@ -446,7 +449,7 @@ def _cmd_mcp_add(args_str: str) -> None:
|
||||
kwargs = parse_mcp_add_args(tokens)
|
||||
entry = add_mcp_server(**kwargs)
|
||||
console.print(f"[green]Added MCP server:[/green] [cyan]{kwargs['name']}[/cyan] ({entry['transport']})")
|
||||
console.print("[dim]Reload the agent with /new to activate.[/dim]")
|
||||
console.print("[dim]Reload with /new to apply.[/dim]")
|
||||
except ValueError as exc:
|
||||
console.print(f"[red]{exc}[/red]")
|
||||
console.print()
|
||||
@@ -478,7 +481,7 @@ def _cmd_mcp_edit(args_str: str) -> None:
|
||||
console.print(f"[green]Updated MCP server:[/green] [cyan]{name}[/cyan]")
|
||||
for k, v in fields.items():
|
||||
console.print(f" [dim]{k}:[/dim] {v}")
|
||||
console.print("[dim]Reload the agent with /new to apply.[/dim]")
|
||||
console.print("[dim]Reload with /new to apply.[/dim]")
|
||||
except KeyError as exc:
|
||||
console.print(f"[red]{exc}[/red]")
|
||||
except ValueError as exc:
|
||||
@@ -497,12 +500,67 @@ def _cmd_mcp_remove(name: str) -> None:
|
||||
|
||||
if remove_mcp_server(name.strip()):
|
||||
console.print(f"[green]Removed MCP server:[/green] [cyan]{name.strip()}[/cyan]")
|
||||
console.print("[dim]Reload the agent with /new to apply.[/dim]")
|
||||
console.print("[dim]Reload with /new to apply.[/dim]")
|
||||
else:
|
||||
console.print(f"[red]Server not found:[/red] {name.strip()}")
|
||||
console.print()
|
||||
|
||||
|
||||
def _cmd_mcp_config(name: str) -> None:
|
||||
"""Handle ``/mcp config [name]``."""
|
||||
from .mcp import load_mcp_config
|
||||
from .mcp.client import USER_MCP_CONFIG
|
||||
|
||||
config = load_mcp_config()
|
||||
if not config:
|
||||
console.print("[dim]No MCP servers configured.[/dim]")
|
||||
console.print()
|
||||
return
|
||||
|
||||
name = name.strip()
|
||||
if name and name not in config:
|
||||
console.print(f"[red]Server not found:[/red] {name}")
|
||||
console.print()
|
||||
return
|
||||
|
||||
servers = {name: config[name]} if name else config
|
||||
|
||||
for srv_name, srv in servers.items():
|
||||
table = Table(
|
||||
title=f"MCP Server: {srv_name}",
|
||||
show_header=True,
|
||||
title_style="bold cyan",
|
||||
)
|
||||
table.add_column("Setting", style="cyan")
|
||||
table.add_column("Value")
|
||||
|
||||
table.add_row("transport", str(srv.get("transport", "(not set)")))
|
||||
if srv.get("command"):
|
||||
table.add_row("command", str(srv["command"]))
|
||||
if srv.get("args"):
|
||||
table.add_row("args", " ".join(str(a) for a in srv["args"]))
|
||||
if srv.get("url"):
|
||||
table.add_row("url", str(srv["url"]))
|
||||
if srv.get("headers"):
|
||||
for k, v in srv["headers"].items():
|
||||
table.add_row(f"header: {k}", str(v))
|
||||
if srv.get("env"):
|
||||
for k, v in srv["env"].items():
|
||||
table.add_row(f"env: {k}", str(v))
|
||||
tools = srv.get("tools")
|
||||
table.add_row("tools", ", ".join(tools) if tools else "[dim](all)[/dim]")
|
||||
expose_to = srv.get("expose_to", ["main"])
|
||||
if isinstance(expose_to, str):
|
||||
expose_to = [expose_to]
|
||||
table.add_row("expose_to", ", ".join(expose_to))
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
console.print(f"[dim]Config file: {USER_MCP_CONFIG}[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
def _cmd_mcp(args: str) -> None:
|
||||
"""Dispatch ``/mcp`` subcommands."""
|
||||
args = args.strip()
|
||||
@@ -515,10 +573,13 @@ def _cmd_mcp(args: str) -> None:
|
||||
_cmd_mcp_edit(args[4:].strip())
|
||||
elif args.startswith("remove"):
|
||||
_cmd_mcp_remove(args[6:].strip())
|
||||
elif args.startswith("config"):
|
||||
_cmd_mcp_config(args[6:].strip())
|
||||
else:
|
||||
console.print("[bold]MCP commands:[/bold]")
|
||||
console.print(" /mcp List configured servers")
|
||||
console.print(" /mcp list List configured servers")
|
||||
console.print(" /mcp config Show detailed server config")
|
||||
console.print(" /mcp add ... Add a server")
|
||||
console.print(" /mcp edit ... Edit an existing server")
|
||||
console.print(" /mcp remove ... Remove a server")
|
||||
@@ -982,7 +1043,37 @@ config_app = typer.Typer(help="Configuration management commands", invoke_withou
|
||||
app.add_typer(config_app, name="config")
|
||||
|
||||
# MCP subcommand group
|
||||
mcp_app = typer.Typer(help="MCP server management commands", invoke_without_command=True)
|
||||
_MCP_HELP = """\
|
||||
Configure and manage MCP servers.
|
||||
|
||||
Examples:
|
||||
# Add a local MCP server (stdio auto-detected):
|
||||
EvoSci mcp add local-server python -- /path/to/server.py
|
||||
|
||||
# Add an npx-based server:
|
||||
EvoSci mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking
|
||||
|
||||
# Add an HTTP server (http auto-detected from URL):
|
||||
EvoSci mcp add docs-langchain https://docs.langchain.com/mcp
|
||||
|
||||
# Add a stdio server with env vars (hardcoded):
|
||||
EvoSci mcp add my-server node --env API_KEY=xxx -- server.js
|
||||
|
||||
# Add a server with runtime env ref (resolved from .env at startup):
|
||||
EvoSci mcp add brave-search npx --env-ref BRAVE_API_KEY -- -y @modelcontextprotocol/server-brave-search
|
||||
|
||||
# Expose to a specific sub-agent (e.g. research-agent):
|
||||
EvoSci mcp add brave-search npx --env-ref BRAVE_API_KEY -e research-agent -- -y @modelcontextprotocol/server-brave-search
|
||||
|
||||
# Expose to multiple agents:
|
||||
EvoSci mcp add local-server python -e main,research-agent,code-agent -- /path/to/server.py
|
||||
|
||||
# Explicit transport override:
|
||||
EvoSci mcp add my-sse https://example.com/sse --transport sse
|
||||
|
||||
Sub-agents (-e): planner-agent | research-agent | code-agent | debug-agent | data-analysis-agent | writing-agent
|
||||
"""
|
||||
mcp_app = typer.Typer(help=_MCP_HELP, invoke_without_command=True)
|
||||
app.add_typer(mcp_app, name="mcp")
|
||||
|
||||
|
||||
@@ -1131,26 +1222,100 @@ def mcp_list():
|
||||
_mcp_list_servers()
|
||||
|
||||
|
||||
@mcp_app.command("config")
|
||||
def mcp_config(
|
||||
name: Optional[str] = typer.Argument(None, help="Server name (omit to show all)"),
|
||||
):
|
||||
"""Show detailed configuration for MCP servers.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
evosci mcp config # Show all servers in detail
|
||||
evosci mcp config filesystem # Show one server
|
||||
"""
|
||||
from .mcp import load_mcp_config
|
||||
from .mcp.client import USER_MCP_CONFIG
|
||||
|
||||
config = load_mcp_config()
|
||||
|
||||
if not config:
|
||||
console.print("[dim]No MCP servers configured.[/dim]")
|
||||
console.print("[dim]Add one with:[/dim] EvoSci mcp add <name> <transport> <command-or-url> [args...]")
|
||||
return
|
||||
|
||||
if name and name not in config:
|
||||
console.print(f"[red]Server not found:[/red] {name}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
servers = {name: config[name]} if name else config
|
||||
|
||||
for srv_name, srv in servers.items():
|
||||
table = Table(
|
||||
title=f"MCP Server: {srv_name}",
|
||||
show_header=True,
|
||||
title_style="bold cyan",
|
||||
)
|
||||
table.add_column("Setting", style="cyan")
|
||||
table.add_column("Value")
|
||||
|
||||
table.add_row("transport", str(srv.get("transport", "(not set)")))
|
||||
|
||||
if srv.get("command"):
|
||||
table.add_row("command", str(srv["command"]))
|
||||
if srv.get("args"):
|
||||
table.add_row("args", " ".join(str(a) for a in srv["args"]))
|
||||
if srv.get("url"):
|
||||
table.add_row("url", str(srv["url"]))
|
||||
if srv.get("headers"):
|
||||
for k, v in srv["headers"].items():
|
||||
table.add_row(f"header: {k}", str(v))
|
||||
if srv.get("env"):
|
||||
for k, v in srv["env"].items():
|
||||
table.add_row(f"env: {k}", str(v))
|
||||
|
||||
tools = srv.get("tools")
|
||||
table.add_row("tools", ", ".join(tools) if tools else "[dim](all)[/dim]")
|
||||
|
||||
expose_to = srv.get("expose_to", ["main"])
|
||||
if isinstance(expose_to, str):
|
||||
expose_to = [expose_to]
|
||||
table.add_row("expose_to", ", ".join(expose_to))
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
console.print(f"[dim]Config file: {USER_MCP_CONFIG}[/dim]")
|
||||
|
||||
|
||||
@mcp_app.command("add")
|
||||
def mcp_add(
|
||||
name: str = typer.Argument(..., help="Server name"),
|
||||
transport: str = typer.Argument(..., help="Transport: stdio, http, sse, websocket"),
|
||||
target: str = typer.Argument(..., help="Command (stdio) or URL (http/sse/websocket)"),
|
||||
target: str = typer.Argument(..., help="Command (stdio) or URL (http/sse)"),
|
||||
args: Optional[list[str]] = typer.Argument(None, help="Extra args for stdio command"),
|
||||
transport: Optional[str] = typer.Option(None, "--transport", "-T", help="Transport type (default: auto-detect)"),
|
||||
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Comma-separated tool allowlist"),
|
||||
expose_to: Optional[str] = typer.Option(None, "--expose-to", "-e", help="Comma-separated target agents"),
|
||||
header: Optional[list[str]] = typer.Option(None, "--header", "-H", help="HTTP header as Key:Value (repeatable)"),
|
||||
env: Optional[list[str]] = typer.Option(None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)"),
|
||||
env_ref: Optional[list[str]] = typer.Option(None, "--env-ref", help="Env var name as ${NAME} runtime ref (repeatable)"),
|
||||
):
|
||||
"""Add an MCP server to user config.
|
||||
|
||||
\b
|
||||
Transport is auto-detected: URLs default to http, commands default to stdio.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
evosci mcp add filesystem stdio npx -- -y @modelcontextprotocol/server-filesystem /tmp
|
||||
evosci mcp add my-api http http://localhost:8080/mcp -H "Authorization:Bearer tok"
|
||||
evosci mcp add my-sse sse http://localhost:9090/sse -e research-agent
|
||||
evosci mcp add sequential-thinking npx -- -y @modelcontextprotocol/server-sequential-thinking
|
||||
evosci mcp add docs-langchain https://docs.langchain.com/mcp
|
||||
evosci mcp add my-sse https://example.com/sse --transport sse -e research-agent
|
||||
evosci mcp add brave-search npx --env-ref BRAVE_API_KEY -- -y @modelcontextprotocol/server-brave-search
|
||||
"""
|
||||
from .mcp import add_mcp_server
|
||||
from .mcp.client import _infer_transport
|
||||
|
||||
if transport is None:
|
||||
transport = _infer_transport(target)
|
||||
|
||||
kwargs: dict = {
|
||||
"name": name,
|
||||
@@ -1160,12 +1325,14 @@ def mcp_add(
|
||||
if transport == "stdio":
|
||||
kwargs["command"] = target
|
||||
kwargs["args"] = list(args) if args else []
|
||||
if env:
|
||||
if env or env_ref:
|
||||
env_dict = {}
|
||||
for e in env:
|
||||
for e in (env or []):
|
||||
if "=" in e:
|
||||
k, v = e.split("=", 1)
|
||||
env_dict[k.strip()] = v.strip()
|
||||
for ref in (env_ref or []):
|
||||
env_dict[ref] = "${" + ref + "}"
|
||||
if env_dict:
|
||||
kwargs["env"] = env_dict
|
||||
else:
|
||||
|
||||
+31
-19
@@ -215,37 +215,46 @@ def remove_mcp_server(name: str) -> bool:
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _infer_transport(target: str) -> str:
|
||||
"""Return ``'http'`` if *target* looks like a URL, else ``'stdio'``."""
|
||||
if target.startswith(("http://", "https://", "ws://", "wss://")):
|
||||
return "http"
|
||||
return "stdio"
|
||||
|
||||
|
||||
def parse_mcp_add_args(tokens: list[str]) -> dict:
|
||||
"""Parse CLI tokens for ``/mcp add`` into kwargs for :func:`add_mcp_server`.
|
||||
|
||||
Syntax::
|
||||
|
||||
<name> <transport> <command-or-url> [extra-args...]
|
||||
[--tools t1,t2] [--expose-to a1,a2] [--header Key:Value]...
|
||||
[--env KEY=VALUE]...
|
||||
<name> <command-or-url> [extra-args...]
|
||||
[--transport T] [--tools t1,t2] [--expose-to a1,a2]
|
||||
[--header Key:Value]... [--env KEY=VALUE]...
|
||||
|
||||
For stdio: positional args after transport are command + args.
|
||||
For http/sse/websocket: first positional arg after transport is url.
|
||||
Transport defaults to ``stdio`` for commands and ``http`` for URLs.
|
||||
"""
|
||||
if len(tokens) < 3:
|
||||
if len(tokens) < 2:
|
||||
raise ValueError(
|
||||
"Usage: <name> <transport> <command-or-url> [args...]\n"
|
||||
" Options: --tools t1,t2 --expose-to agent1,agent2 --header Key:Value --env KEY=VALUE"
|
||||
"Usage: <name> <command-or-url> [args...]\n"
|
||||
" Options: --transport T --tools t1,t2 --expose-to agent1,agent2 --header Key:Value --env KEY=VALUE"
|
||||
)
|
||||
|
||||
name = tokens[0]
|
||||
transport = tokens[1]
|
||||
|
||||
positional: list[str] = []
|
||||
transport: str | None = None
|
||||
tools: list[str] | None = None
|
||||
expose_to: list[str] | None = None
|
||||
headers: dict[str, str] = {}
|
||||
env: dict[str, str] = {}
|
||||
|
||||
i = 2
|
||||
i = 1
|
||||
while i < len(tokens):
|
||||
tok = tokens[i]
|
||||
if tok == "--tools" and i + 1 < len(tokens):
|
||||
if tok in ("--transport", "-T") and i + 1 < len(tokens):
|
||||
transport = tokens[i + 1]
|
||||
i += 2
|
||||
elif tok == "--tools" and i + 1 < len(tokens):
|
||||
tools = [t.strip() for t in tokens[i + 1].split(",") if t.strip()]
|
||||
i += 2
|
||||
elif tok == "--expose-to" and i + 1 < len(tokens):
|
||||
@@ -263,26 +272,29 @@ def parse_mcp_add_args(tokens: list[str]) -> dict:
|
||||
k, v = kv.split("=", 1)
|
||||
env[k.strip()] = v.strip()
|
||||
i += 2
|
||||
elif tok == "--env-ref" and i + 1 < len(tokens):
|
||||
env[tokens[i + 1]] = "${" + tokens[i + 1] + "}"
|
||||
i += 2
|
||||
elif tok == "--":
|
||||
i += 1 # skip -- separator (used by shells, not meaningful here)
|
||||
else:
|
||||
positional.append(tok)
|
||||
i += 1
|
||||
|
||||
if not positional:
|
||||
raise ValueError("A command or URL is required after the server name")
|
||||
|
||||
if transport is None:
|
||||
transport = _infer_transport(positional[0])
|
||||
|
||||
kwargs: dict = {"name": name, "transport": transport}
|
||||
|
||||
if transport == "stdio":
|
||||
if not positional:
|
||||
raise ValueError(
|
||||
"stdio transport requires a command after the transport name"
|
||||
)
|
||||
kwargs["command"] = positional[0]
|
||||
kwargs["args"] = positional[1:]
|
||||
if env:
|
||||
kwargs["env"] = env
|
||||
else:
|
||||
if not positional:
|
||||
raise ValueError(
|
||||
f"{transport} transport requires a url after the transport name"
|
||||
)
|
||||
kwargs["url"] = positional[0]
|
||||
if headers:
|
||||
kwargs["headers"] = headers
|
||||
|
||||
+66
-2
@@ -53,7 +53,7 @@ CONFIRM_STYLE = Style.from_dict({
|
||||
"text": "",
|
||||
})
|
||||
|
||||
STEPS = ["Provider", "API Key", "Model", "Tavily Key", "Workspace", "Parameters", "Skills", "Channels"]
|
||||
STEPS = ["Provider", "API Key", "Model", "Tavily Key", "Workspace", "Parameters", "Skills", "MCP Servers", "Channels"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -781,6 +781,67 @@ def _step_skills() -> list[str]:
|
||||
return installed
|
||||
|
||||
|
||||
_RECOMMENDED_MCP_SERVERS = [
|
||||
{
|
||||
"label": "Sequential Thinking (structured reasoning for non-reasoning models)",
|
||||
"name": "sequential-thinking",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||
},
|
||||
{
|
||||
"label": "Docs by LangChain (documentation for building agents)",
|
||||
"name": "docs-langchain",
|
||||
"url": "https://docs.langchain.com/mcp",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _step_mcp_servers() -> list[str]:
|
||||
"""Step 8: Optionally install recommended MCP servers.
|
||||
|
||||
Shows a checkbox list of recommended servers. Selected ones are added
|
||||
to the user MCP config via ``add_mcp_server()``.
|
||||
|
||||
Returns:
|
||||
List of server names that were installed.
|
||||
"""
|
||||
choices = [
|
||||
Choice(title=srv["label"], value=srv["name"])
|
||||
for srv in _RECOMMENDED_MCP_SERVERS
|
||||
]
|
||||
|
||||
selected = questionary.checkbox(
|
||||
"Install recommended MCP servers:",
|
||||
choices=choices,
|
||||
style=WIZARD_STYLE,
|
||||
).ask()
|
||||
|
||||
if selected is None:
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
if not selected:
|
||||
_print_step_skipped("MCP Servers", "none selected")
|
||||
console.print(" [dim]Add later with: EvoSci mcp add <name> <command> [--env-ref KEY] -- [args][/dim]")
|
||||
return []
|
||||
|
||||
from .mcp.client import add_mcp_server
|
||||
|
||||
installed = []
|
||||
for name in selected:
|
||||
srv = next(s for s in _RECOMMENDED_MCP_SERVERS if s["name"] == name)
|
||||
try:
|
||||
if "url" in srv:
|
||||
add_mcp_server(name, "streamable_http", url=srv["url"])
|
||||
else:
|
||||
add_mcp_server(name, "stdio", command=srv["command"], args=srv["args"])
|
||||
_print_step_result("MCP", f"{name}")
|
||||
installed.append(name)
|
||||
except Exception as e:
|
||||
_print_step_result("MCP", f"{name} — {e}", success=False)
|
||||
|
||||
return installed
|
||||
|
||||
|
||||
def validate_imessage() -> tuple[bool, str]:
|
||||
"""Validate iMessage environment by checking for the imsg CLI.
|
||||
|
||||
@@ -1091,7 +1152,10 @@ def run_onboard(skip_validation: bool = False) -> bool:
|
||||
# Step 7: Skills
|
||||
_step_skills()
|
||||
|
||||
# Step 8: Channels
|
||||
# Step 8: MCP Servers
|
||||
_step_mcp_servers()
|
||||
|
||||
# Step 9: Channels
|
||||
imessage_enabled, imessage_allowed_senders = _step_channels(config)
|
||||
config.imessage_enabled = imessage_enabled
|
||||
config.imessage_allowed_senders = imessage_allowed_senders
|
||||
|
||||
+43
-14
@@ -381,47 +381,76 @@ class TestRemoveMcpServer:
|
||||
|
||||
class TestParseMcpAddArgs:
|
||||
def test_stdio_basic(self):
|
||||
r = parse_mcp_add_args(["fs", "stdio", "npx", "-y", "server", "/tmp"])
|
||||
r = parse_mcp_add_args(["fs", "npx", "-y", "server", "/tmp"])
|
||||
assert r["name"] == "fs"
|
||||
assert r["transport"] == "stdio"
|
||||
assert r["command"] == "npx"
|
||||
assert r["args"] == ["-y", "server", "/tmp"]
|
||||
|
||||
def test_http_basic(self):
|
||||
r = parse_mcp_add_args(["api", "http", "http://localhost:8080/mcp"])
|
||||
def test_http_auto_detected(self):
|
||||
r = parse_mcp_add_args(["api", "http://localhost:8080/mcp"])
|
||||
assert r["transport"] == "http"
|
||||
assert r["url"] == "http://localhost:8080/mcp"
|
||||
|
||||
def test_https_auto_detected(self):
|
||||
r = parse_mcp_add_args(["api", "https://example.com/mcp"])
|
||||
assert r["transport"] == "http"
|
||||
assert r["url"] == "https://example.com/mcp"
|
||||
|
||||
def test_ws_auto_detected(self):
|
||||
r = parse_mcp_add_args(["ws", "ws://localhost:9090"])
|
||||
assert r["transport"] == "http"
|
||||
|
||||
def test_explicit_transport_override(self):
|
||||
r = parse_mcp_add_args(["srv", "https://example.com/sse", "--transport", "sse"])
|
||||
assert r["transport"] == "sse"
|
||||
assert r["url"] == "https://example.com/sse"
|
||||
|
||||
def test_explicit_transport_short_flag(self):
|
||||
r = parse_mcp_add_args(["srv", "https://x", "-T", "websocket"])
|
||||
assert r["transport"] == "websocket"
|
||||
|
||||
def test_tools_flag(self):
|
||||
r = parse_mcp_add_args(["srv", "http", "http://x", "--tools", "a,b"])
|
||||
r = parse_mcp_add_args(["srv", "http://x", "--tools", "a,b"])
|
||||
assert r["tools"] == ["a", "b"]
|
||||
|
||||
def test_expose_to_flag(self):
|
||||
r = parse_mcp_add_args(
|
||||
["srv", "http", "http://x", "--expose-to", "main,code-agent"]
|
||||
["srv", "http://x", "--expose-to", "main,code-agent"]
|
||||
)
|
||||
assert r["expose_to"] == ["main", "code-agent"]
|
||||
|
||||
def test_header_flag(self):
|
||||
r = parse_mcp_add_args(
|
||||
["srv", "http", "http://x", "--header", "Authorization:Bearer tok"]
|
||||
["srv", "http://x", "--header", "Authorization:Bearer tok"]
|
||||
)
|
||||
assert r["headers"] == {"Authorization": "Bearer tok"}
|
||||
|
||||
def test_env_flag(self):
|
||||
r = parse_mcp_add_args(["srv", "stdio", "cmd", "--env", "FOO=bar"])
|
||||
r = parse_mcp_add_args(["srv", "cmd", "--env", "FOO=bar"])
|
||||
assert r["env"] == {"FOO": "bar"}
|
||||
|
||||
def test_too_few_tokens_raises(self):
|
||||
with pytest.raises(ValueError, match="Usage"):
|
||||
parse_mcp_add_args(["fs", "stdio"])
|
||||
parse_mcp_add_args(["fs"])
|
||||
|
||||
def test_stdio_missing_command_raises(self):
|
||||
with pytest.raises(ValueError, match="requires a command"):
|
||||
parse_mcp_add_args(["fs", "stdio", "--tools", "a"])
|
||||
def test_double_dash_ignored(self):
|
||||
r = parse_mcp_add_args(["srv", "npx", "--", "-y", "pkg"])
|
||||
assert r["command"] == "npx"
|
||||
assert r["args"] == ["-y", "pkg"]
|
||||
assert "--" not in r["args"]
|
||||
|
||||
def test_http_missing_url_raises(self):
|
||||
with pytest.raises(ValueError, match="requires a url"):
|
||||
parse_mcp_add_args(["srv", "http", "--tools", "a"])
|
||||
def test_env_ref_flag(self):
|
||||
r = parse_mcp_add_args(["srv", "cmd", "--env-ref", "FOO"])
|
||||
assert r["env"] == {"FOO": "${FOO}"}
|
||||
|
||||
def test_env_ref_and_env_combined(self):
|
||||
r = parse_mcp_add_args(["srv", "cmd", "--env", "DEBUG=true", "--env-ref", "API_KEY"])
|
||||
assert r["env"] == {"DEBUG": "true", "API_KEY": "${API_KEY}"}
|
||||
|
||||
def test_missing_command_or_url_raises(self):
|
||||
with pytest.raises(ValueError, match="command or URL is required"):
|
||||
parse_mcp_add_args(["fs", "--tools", "a"])
|
||||
|
||||
|
||||
# ---- edit_mcp_server ----
|
||||
|
||||
@@ -22,10 +22,10 @@ from EvoScientist.config import EvoScientistConfig
|
||||
|
||||
|
||||
class TestConstants:
|
||||
def test_steps_has_eight_items(self):
|
||||
"""Test that STEPS contains exactly 8 steps."""
|
||||
assert len(STEPS) == 8
|
||||
assert STEPS == ["Provider", "API Key", "Model", "Tavily Key", "Workspace", "Parameters", "Skills", "Channels"]
|
||||
def test_steps_has_nine_items(self):
|
||||
"""Test that STEPS contains exactly 9 steps."""
|
||||
assert len(STEPS) == 9
|
||||
assert STEPS == ["Provider", "API Key", "Model", "Tavily Key", "Workspace", "Parameters", "Skills", "MCP Servers", "Channels"]
|
||||
|
||||
def test_wizard_style_is_style_instance(self):
|
||||
"""Test that WIZARD_STYLE is a prompt_toolkit Style."""
|
||||
|
||||
Reference in New Issue
Block a user