Source code for neurocore.cli.mcp_cmd
"""neurocore mcp — interact with MCP servers from the CLI.
These commands require the optional ``neurocore-skill-mcp`` package:
pip install neurocore-skill-mcp
Usage:
neurocore mcp list-tools --command docker --args "run,-i,--rm,server-image"
neurocore mcp list-tools --url https://example.com/mcp
neurocore mcp call <tool> --command ... --arg key=value
"""
from __future__ import annotations
import asyncio
import json
from typing import Any, Optional
import typer
from rich.console import Console
from rich.table import Table
console = Console(stderr=True)
output_console = Console()
mcp_app = typer.Typer(
name="mcp",
help="Inspect and call MCP server tools (needs neurocore-skill-mcp).",
no_args_is_help=True,
)
def _import_client() -> Any:
try:
from neurocore_skill_mcp import client
except ImportError:
console.print(
"[red]MCP support is not installed.[/red] "
"Run: pip install neurocore-skill-mcp"
)
raise typer.Exit(code=1) from None
return client
def _build_cfg(
command: Optional[str], args: str, url: Optional[str]
) -> dict[str, object]:
if url:
return {"transport": "http", "url": url}
if command:
arg_list = [a for a in args.split(",") if a] if args else []
return {"transport": "stdio", "command": command, "args": arg_list}
raise typer.BadParameter("Provide either --command (stdio) or --url (http).")
[docs]
@mcp_app.command("list-tools")
def list_tools(
command: Optional[str] = typer.Option(None, "--command", help="stdio executable."),
args: str = typer.Option("", "--args", help="Comma-separated args for --command."),
url: Optional[str] = typer.Option(None, "--url", help="streamable HTTP endpoint."),
) -> None:
"""List the tools a server exposes."""
client = _import_client()
cfg = _build_cfg(command, args, url)
async def _run() -> list[Any]:
async with client.open_session(cfg) as session:
result = await session.list_tools()
return list(result.tools)
try:
tools = asyncio.run(_run())
except Exception as e: # noqa: BLE001
console.print(f"[red]Failed to list tools:[/red] {e}")
raise typer.Exit(code=1) from None
if not tools:
console.print("[dim]No tools exposed.[/dim]")
return
table = Table(title="MCP tools")
table.add_column("Name", style="cyan")
table.add_column("Description")
for tool in tools:
table.add_row(tool.name, (getattr(tool, "description", "") or "")[:80])
output_console.print(table)
[docs]
@mcp_app.command("call")
def call_tool(
tool: str = typer.Argument(help="Tool name to invoke."),
arg: list[str] = typer.Option([], "--arg", help="Tool argument KEY=VALUE."),
command: Optional[str] = typer.Option(None, "--command", help="stdio executable."),
args: str = typer.Option("", "--args", help="Comma-separated args for --command."),
url: Optional[str] = typer.Option(None, "--url", help="streamable HTTP endpoint."),
) -> None:
"""Call a single tool and print its result."""
client = _import_client()
cfg = _build_cfg(command, args, url)
call_args: dict[str, str] = {}
for item in arg:
key, _, value = item.partition("=")
call_args[key.strip()] = value
async def _run() -> object:
async with client.open_session(cfg) as session:
response = await session.call_tool(tool, call_args)
return client.normalize_content(response)
try:
result = asyncio.run(_run())
except Exception as e: # noqa: BLE001
console.print(f"[red]Tool call failed:[/red] {e}")
raise typer.Exit(code=1) from None
typer.echo(json.dumps(result, indent=2, default=str))