8b1451cdda
* 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>
102 lines
3.4 KiB
Python
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",
|
|
)
|