Files
EvoScientist-Multi/EvoScientist/commands/implementation/mcp_install.py
T
dinos 8b1451cdda refactor(runtime): centralize async bridges under an owned runtime (#376)
* feat(runtime): add application-scoped async runtime

* refactor(cli): use owned runtime for session stats

* refactor(onboard): use the owned async runtime

* docs(runtime): record async bridge ownership

* refactor(middleware): keep sync fallback synchronous

* refactor(mcp): load tools on an owned runtime

* refactor(cli): share owned runtime across entry points

* refactor(channels): make inbound sync bridge explicit

* refactor(stream): run Rich streaming on owned runtime

* chore(runtime): remove nest-asyncio dependency

* refactor(asyncio): require active loops in async code

* docs(runtime): document final event loop ownership

* fix(stream): cancel stalled owned streams

* fix(cli): recover cleanly from stream cancellation

* fix(runtime): drain executor work before shutdown

* fix(runtime): terminate cancelled shell process trees

* fix(models): let fallback bypass selector failures

* fix(cli): reset interrupt handling between turns

* docs: rm implementation spec

* fix(serve): cancel active turns during shutdown

* fix(runtime): protect settlement from waiter cancellation

* fix(backends): reject empty shell commands

* fix(runtime): terminate descendants after shell exit

* fix(mcp): keep standalone discovery off channel loop

* fix(cli): own and settle interactive prompt cancellation

* fix(serve): keep channel sends off runtime loop

* fix(stream): scope cancel context to iterator steps

* refactor(serve): require the owned async runtime

* fix(channels): keep interactive sends off runtime loop

* fix(selector): surface fallback without log spam

* test(runtime): normalize Windows shell marker

* fix(cli): serialize interactive session turns

* fix(shell): bound output drain after termination

* fix(ui): do not retry owned runtime failures

* fix(shell): allow signal-safe registry reentry

* fix(shell): avoid terminating reused process ids

* fix(channels): preserve streaming send order

* fix(cli): report runtime shutdown timeouts cleanly

* fix(mcp): guide async callers to async loader

* docs(runtime): clarify reserved async bridge APIs

* fix(runtime): bound code interpreter cleanup

* test(shell): use active Python for drain regression

---------

Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
2026-07-27 14:17:57 +01:00

102 lines
3.4 KiB
Python

from __future__ import annotations
from typing import ClassVar
from ..base import Argument, Command, CommandContext
class InstallMCPCommand(Command):
"""Browse and install MCP servers."""
name = "/install-mcp"
description = "Browse and install MCP servers"
category = "MCP"
arguments: ClassVar[list[Argument]] = [
Argument(
name="source",
type=str,
description="Server name or tag filter",
required=False,
)
]
async def execute(self, ctx: CommandContext, args: list[str]) -> None:
from ...mcp.registry import (
fetch_marketplace_index,
find_server_by_name,
get_all_tags,
get_installed_names,
install_mcp_server,
install_mcp_servers,
)
source = args[0] if args else ""
ctx.ui.append_system("Fetching MCP server index...", style="dim")
await ctx.ui.flush()
try:
import asyncio
servers = await asyncio.get_running_loop().run_in_executor(
None, fetch_marketplace_index
)
except Exception as e:
ctx.ui.append_system(f"Failed to fetch server index: {e}", style="red")
return
if not servers:
ctx.ui.append_system("No MCP servers found.", style="yellow")
return
# Direct name match
if source:
match = find_server_by_name(source, servers)
if match:
installed = get_installed_names()
if match.name in installed:
ctx.ui.append_system(
f"{match.name} is already configured.", style="yellow"
)
return
if install_mcp_server(match, print_fn=ctx.ui.append_system):
ctx.ui.append_system(f"Configured: {match.name}", style="green")
ctx.ui.append_system("Reload with /new to apply.", style="dim")
else:
ctx.ui.append_system(
f"Failed to configure {match.name}.", style="red"
)
return
# Check if it's a tag — fall through to browser
if source.lower() not in get_all_tags(servers):
ctx.ui.append_system(
f"No server or tag found matching: {source}", style="red"
)
close = [s.name for s in servers if source.lower() in s.name.lower()]
if close:
ctx.ui.append_system(
f"Did you mean: {', '.join(close)}?", style="dim"
)
return
# Interactive browse (or pre-filtered by tag)
installed_names = get_installed_names()
selected_entries = await ctx.ui.wait_for_mcp_browse(
servers, installed_names, pre_filter_tag=source
)
if selected_entries is None:
ctx.ui.append_system("Browse cancelled.", style="dim")
return
if not selected_entries:
ctx.ui.append_system("No servers selected.", style="dim")
return
count = install_mcp_servers(selected_entries, print_fn=ctx.ui.append_system)
if count:
ctx.ui.append_system(
f"{count} server(s) configured. Reload with /new to apply.",
style="green",
)