Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ mcpm search [QUERY] # Search the MCP Registry for available servers
mcpm add SERVER_URL # Add an MCP server configuration (from URL or registry name)
mcpm add SERVER_URL --alias ALIAS # Add with a custom alias

# 🛠️ Add custom server
mcpm import stdio SERVER_NAME --command COMMAND --args ARGS --env ENV # Add a stdio MCP server to a client
mcpm import sse SERVER_NAME --url URL # Add a SSE MCP server to a client
mcpm import interact # Add a server by configuring it interactively

# 📋 List and Remove
mcpm ls # List server configurations for the active client/profile
mcpm rm SERVER_NAME # Remove a server configuration
Expand Down
5 changes: 5 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ mcpm search [QUERY] # 在 MCP 注册表中搜索可用服务器
mcpm add SERVER_URL # 添加 MCP 服务器配置(从 URL 或注册表名称)
mcpm add SERVER_URL --alias ALIAS # 添加并使用自定义别名

# 🛠️ 自定义添加
mcpm import stdio SERVER_NAME --command COMMAND --args ARGS --env ENV # 手动添加一个 stdio MCP 服务器
mcpm import sse SERVER_NAME --url URL # 手动添加一个 SSE MCP 服务器
mcpm import interact # 通过交互式添加一个服务器

# 📋 列出和删除
mcpm ls # 列出活动客户端/配置文件的服务器配置
mcpm rm SERVER_NAME # 删除服务器配置
Expand Down
5 changes: 4 additions & 1 deletion src/mcpm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
add,
client,
config,
custom,
info,
inspector,
list,
Expand Down Expand Up @@ -149,7 +150,8 @@ def main(ctx, help_flag, version):
commands_table.add_row("[yellow]server[/]")
commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.")
commands_table.add_row(" [cyan]info[/]", "Show detailed information about a specific MCP server.")
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client.")
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client/profile.")
commands_table.add_row(" [cyan]import[/]", "Import a custom MCP server to a client/profile.")
commands_table.add_row(" [cyan]cp[/]", "Copy a server from one client/profile to another.")
commands_table.add_row(" [cyan]mv[/]", "Move a server from one client/profile to another.")
commands_table.add_row(" [cyan]rm[/]", "Remove an installed MCP server.")
Expand Down Expand Up @@ -194,6 +196,7 @@ def main(ctx, help_flag, version):
main.add_command(profile.activate)
main.add_command(profile.deactivate)
main.add_command(router.router, name="router")
main.add_command(custom.import_server, name="import")

if __name__ == "__main__":
main()
19 changes: 17 additions & 2 deletions src/mcpm/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,23 @@
MCPM commands package
"""

__all__ = ["add", "client", "inspector", "list", "pop", "profile", "remove", "search", "stash", "transfer", "router"]
__all__ = [
"add",
"client",
"inspector",
"list",
"pop",
"profile",
"remove",
"search",
"stash",
"transfer",
"router",
"custom",
]

# All command modules


from . import client, inspector, list, profile, router, search
from .server_operations import add, pop, remove, stash, transfer
from .server_operations import add, custom, pop, remove, stash, transfer
16 changes: 6 additions & 10 deletions src/mcpm/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

from mcpm.clients.client_config import ClientConfigManager
from mcpm.clients.client_registry import ClientRegistry
from mcpm.commands.server_operations.common import determine_scope
from mcpm.profile.profile_config import ProfileConfigManager
from mcpm.schemas.server_config import ServerConfig
from mcpm.utils.display import print_active_scope, print_client_error, print_no_active_scope, print_server_config
from mcpm.utils.scope import ScopeType, extract_from_scope, format_scope
from mcpm.utils.display import print_client_error, print_server_config
from mcpm.utils.scope import ScopeType, format_scope

console = Console()
client_config_manager = ClientConfigManager()
Expand All @@ -29,14 +30,9 @@ def list(target: str | None = None):
mcpm ls
mcpm ls -t @cursor
"""
if target is None:
target = ClientRegistry.determine_active_scope()
if not target:
print_no_active_scope()
return
print_active_scope(target)

scope_type, scope = extract_from_scope(target)
scope_type, scope = determine_scope(target)
if not scope:
return

if scope_type == ScopeType.CLIENT:
# Get the active client manager and information
Expand Down
16 changes: 5 additions & 11 deletions src/mcpm/commands/server_operations/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
from rich.prompt import Confirm

from mcpm.clients.client_registry import ClientRegistry
from mcpm.commands.server_operations.common import client_add_server, profile_add_server
from mcpm.commands.server_operations.common import client_add_server, determine_scope, profile_add_server
from mcpm.profile.profile_config import ProfileConfigManager
from mcpm.schemas.full_server_config import FullServerConfig
from mcpm.utils.display import print_active_scope, print_no_active_scope
from mcpm.utils.repository import RepositoryManager
from mcpm.utils.scope import ScopeType, extract_from_scope
from mcpm.utils.scope import ScopeType

console = Console()
repo_manager = RepositoryManager()
Expand Down Expand Up @@ -106,15 +105,10 @@ def add(server_name, force=False, alias=None, target: str | None = None):
"""
config_name = alias or server_name

if target is None:
# Get the active scope
target = ClientRegistry.determine_active_scope()
if not target:
print_no_active_scope()
return
print_active_scope(target)
scope_type, scope = determine_scope(target)
if not scope:
return

scope_type, scope = extract_from_scope(target)
if scope_type == ScopeType.PROFILE:
# Get profile
profile = scope
Expand Down
20 changes: 15 additions & 5 deletions src/mcpm/commands/server_operations/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@
console = Console()


def determine_scope(scope: str | None) -> tuple[ScopeType | None, str | None]:
if not scope:
# Get the active scope
scope = ClientRegistry.determine_active_scope()
if not scope:
print_no_active_scope()
return None, None
print_active_scope(scope)

scope_type, scope = extract_from_scope(scope)
return scope_type, scope


def determine_target(target: str) -> tuple[ScopeType | None, str | None, str | None]:
scope_type, scope, server_name = parse_server(target)
if not scope:
active_scope = ClientRegistry.determine_active_scope()
if not active_scope:
print_no_active_scope()
scope_type, scope = determine_scope(scope)
if not scope:
return None, None, None
scope_type, scope = extract_from_scope(active_scope)
print_active_scope(active_scope)
return scope_type, scope, server_name


Expand Down
206 changes: 206 additions & 0 deletions src/mcpm/commands/server_operations/custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import shlex

import click
from rich.console import Console
from rich.prompt import Confirm, Prompt

from mcpm.commands.server_operations.common import client_add_server, determine_scope, profile_add_server
from mcpm.schemas.server_config import SSEServerConfig, STDIOServerConfig
from mcpm.utils.display import print_server_config
from mcpm.utils.scope import ScopeType

console = Console()


@click.group()
@click.help_option("-h", "--help")
def import_server():
"""Add server definitions manually."""
pass


@import_server.command()
@click.argument("server_name", required=True)
@click.option("--command", "-c", help="Executable command", required=True)
@click.option("--args", "-a", multiple=True, help="Arguments for the command (can be used multiple times)")
@click.option("--env", "-e", multiple=True, help="Environment variables, format: ENV=val (can be used multiple times)")
@click.option("--target", "-t", help="Target client or profile")
@click.option("--force", is_flag=True, help="Force reinstall if server is already installed")
@click.help_option("-h", "--help")
def stdio(server_name, command, args, env, target, force):
"""Add a server by specifying command, args, and env variables.
Examples:

\b
mcpm import stdio <server_name> --command <command> --args <arg1> --args <arg2> --env <var1>=<value1> --env <var2>=<value2>
"""
scope_type, scope = determine_scope(target)
if not scope:
return

# Extract env variables
env_vars = {}
for item in env:
if "=" in item:
key, value = item.split("=", 1)
env_vars[key] = value
else:
console.print(f"[yellow]Ignoring invalid env: {item}[/]")

try:
# support spaces and quotes in args
parsed_args = shlex.split(" ".join(args)) if args else []
server_config = STDIOServerConfig(
name=server_name,
command=command,
args=parsed_args,
env=env_vars,
)
print_server_config(server_config)
except ValueError as e:
console.print(f"[bold red]Error:[/] {e}")
return

if not Confirm.ask(f"Add this server to {scope_type} {scope}?"):
return
console.print(f"[green]Importing server to {scope_type} {scope}[/]")

if scope_type == ScopeType.CLIENT:
success = client_add_server(scope, server_config, force)
else:
success = profile_add_server(scope, server_config, force)

if success:
console.print(f"[bold green]Stdio server '{server_name}' added successfully to {scope_type} {scope}.")
else:
console.print(f"[bold red]Failed to add stdio server '{server_name}' to {scope_type} {scope}.")


@import_server.command()
@click.argument("server_name", required=True)
@click.option("--url", "-u", required=True, help="Server URL")
@click.option("--header", "-H", multiple=True, help="HTTP headers, format: KEY=val (can be used multiple times)")
@click.option("--target", "-t", help="Target to import server to")
@click.option("--force", is_flag=True, help="Force reinstall if server is already installed")
@click.help_option("-h", "--help")
def sse(server_name, url, header, target, force):
"""Add a server by specifying a URL and headers.
Examples:

\b
mcpm import sse <server_name> --url <url> --header <key1>=<value1> --header <key2>=<value2>
"""
scope_type, scope = determine_scope(target)
if not scope:
return

headers = {}
for item in header:
if "=" in item:
key, value = item.split("=", 1)
headers[key] = value
else:
console.print(f"[yellow]Ignoring invalid header: {item}[/]")

try:
server_config = SSEServerConfig(
name=server_name,
url=url,
headers=headers,
)
print_server_config(server_config)
except ValueError as e:
console.print(f"[bold red]Error:[/] {e}")
return

if not Confirm.ask(f"Add this server to {scope_type} {scope}?"):
return
console.print(f"[green]Importing server to {scope_type} {scope}[/]")

if scope_type == ScopeType.CLIENT:
success = client_add_server(scope, server_config, force)
else:
success = profile_add_server(scope, server_config, force)

if success:
console.print(f"[bold green]SSE server '{server_name}' added successfully to {scope_type} {scope}.")
else:
console.print(f"[bold red]Failed to add SSE server '{server_name}' to {scope_type} {scope}.")


@import_server.command()
@click.option("--target", "-t", help="Target to import server to")
@click.help_option("-h", "--help")
def interact(target: str | None = None):
"""Add a server by manually configuring it interactively."""
scope_type, scope = determine_scope(target)
if not scope:
return

server_name = Prompt.ask("Enter server name")
if not server_name:
console.print("[red]Server name cannot be empty.[/]")
return

config_type = Prompt.ask("Select server type", choices=["stdio", "sse"], default="stdio")

if config_type == "stdio":
command = Prompt.ask("Enter command (executable)")
args = Prompt.ask("Enter arguments (space-separated, optional)", default="")
env_input = Prompt.ask("Enter env variables (format: KEY=VAL, comma-separated, optional)", default="")
env = {}
if env_input.strip():
for pair in env_input.split(","):
if "=" in pair:
k, v = pair.split("=", 1)
env[k.strip()] = v.strip()
try:
# support spaces and quotes in args
parsed_args = shlex.split(args) if args.strip() else []
server_config = STDIOServerConfig(
name=server_name,
command=command,
args=parsed_args,
env=env,
)
except ValueError as e:
console.print(f"[bold red]Error:[/] {e}")
return
elif config_type == "sse":
url = Prompt.ask("Enter SSE server URL")
headers_input = Prompt.ask("Enter HTTP headers (format: KEY=VAL, comma-separated, optional)", default="")
headers = {}
if headers_input.strip():
for pair in headers_input.split(","):
if "=" in pair:
k, v = pair.split("=", 1)
headers[k.strip()] = v.strip()
try:
server_config = SSEServerConfig(
name=server_name,
url=url,
headers=headers,
)
except ValueError as e:
console.print(f"[bold red]Error:[/] {e}")
return
else:
console.print(f"[red]Unknown server type: {config_type}[/]")
return

print_server_config(server_config)
if not Confirm.ask(f"Add this server to {scope_type} {scope}?"):
return
console.print(f"[green]Importing server to {scope_type} {scope}[/]")

if scope_type == ScopeType.CLIENT:
success = client_add_server(scope, server_config, False)
else:
success = profile_add_server(scope, server_config, False)

if success:
console.print(
f"[bold green]{config_type.upper()} server '{server_name}' added successfully to {scope_type} {scope}."
)
else:
console.print(f"[bold red]Failed to add {config_type.upper()} server '{server_name}' to {scope_type} {scope}.")
Loading