Skip to content

Commit 80bd9c0

Browse files
feat: support custom add server (#125)
1 parent e3b244c commit 80bd9c0

File tree

10 files changed

+270
-55
lines changed

10 files changed

+270
-55
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ mcpm search [QUERY] # Search the MCP Registry for available servers
9292
mcpm add SERVER_URL # Add an MCP server configuration (from URL or registry name)
9393
mcpm add SERVER_URL --alias ALIAS # Add with a custom alias
9494

95+
# 🛠️ Add custom server
96+
mcpm import stdio SERVER_NAME --command COMMAND --args ARGS --env ENV # Add a stdio MCP server to a client
97+
mcpm import sse SERVER_NAME --url URL # Add a SSE MCP server to a client
98+
mcpm import interact # Add a server by configuring it interactively
99+
95100
# 📋 List and Remove
96101
mcpm ls # List server configurations for the active client/profile
97102
mcpm rm SERVER_NAME # Remove a server configuration

README.zh-CN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ mcpm search [QUERY] # 在 MCP 注册表中搜索可用服务器
126126
mcpm add SERVER_URL # 添加 MCP 服务器配置(从 URL 或注册表名称)
127127
mcpm add SERVER_URL --alias ALIAS # 添加并使用自定义别名
128128

129+
# 🛠️ 自定义添加
130+
mcpm import stdio SERVER_NAME --command COMMAND --args ARGS --env ENV # 手动添加一个 stdio MCP 服务器
131+
mcpm import sse SERVER_NAME --url URL # 手动添加一个 SSE MCP 服务器
132+
mcpm import interact # 通过交互式添加一个服务器
133+
129134
# 📋 列出和删除
130135
mcpm ls # 列出活动客户端/配置文件的服务器配置
131136
mcpm rm SERVER_NAME # 删除服务器配置

src/mcpm/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
add,
1313
client,
1414
config,
15+
custom,
1516
info,
1617
inspector,
1718
list,
@@ -149,7 +150,8 @@ def main(ctx, help_flag, version):
149150
commands_table.add_row("[yellow]server[/]")
150151
commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.")
151152
commands_table.add_row(" [cyan]info[/]", "Show detailed information about a specific MCP server.")
152-
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client.")
153+
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client/profile.")
154+
commands_table.add_row(" [cyan]import[/]", "Import a custom MCP server to a client/profile.")
153155
commands_table.add_row(" [cyan]cp[/]", "Copy a server from one client/profile to another.")
154156
commands_table.add_row(" [cyan]mv[/]", "Move a server from one client/profile to another.")
155157
commands_table.add_row(" [cyan]rm[/]", "Remove an installed MCP server.")
@@ -194,6 +196,7 @@ def main(ctx, help_flag, version):
194196
main.add_command(profile.activate)
195197
main.add_command(profile.deactivate)
196198
main.add_command(router.router, name="router")
199+
main.add_command(custom.import_server, name="import")
197200

198201
if __name__ == "__main__":
199202
main()

src/mcpm/commands/__init__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,23 @@
22
MCPM commands package
33
"""
44

5-
__all__ = ["add", "client", "inspector", "list", "pop", "profile", "remove", "search", "stash", "transfer", "router"]
5+
__all__ = [
6+
"add",
7+
"client",
8+
"inspector",
9+
"list",
10+
"pop",
11+
"profile",
12+
"remove",
13+
"search",
14+
"stash",
15+
"transfer",
16+
"router",
17+
"custom",
18+
]
619

720
# All command modules
21+
22+
823
from . import client, inspector, list, profile, router, search
9-
from .server_operations import add, pop, remove, stash, transfer
24+
from .server_operations import add, custom, pop, remove, stash, transfer

src/mcpm/commands/list.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
from mcpm.clients.client_config import ClientConfigManager
1010
from mcpm.clients.client_registry import ClientRegistry
11+
from mcpm.commands.server_operations.common import determine_scope
1112
from mcpm.profile.profile_config import ProfileConfigManager
1213
from mcpm.schemas.server_config import ServerConfig
13-
from mcpm.utils.display import print_active_scope, print_client_error, print_no_active_scope, print_server_config
14-
from mcpm.utils.scope import ScopeType, extract_from_scope, format_scope
14+
from mcpm.utils.display import print_client_error, print_server_config
15+
from mcpm.utils.scope import ScopeType, format_scope
1516

1617
console = Console()
1718
client_config_manager = ClientConfigManager()
@@ -29,14 +30,9 @@ def list(target: str | None = None):
2930
mcpm ls
3031
mcpm ls -t @cursor
3132
"""
32-
if target is None:
33-
target = ClientRegistry.determine_active_scope()
34-
if not target:
35-
print_no_active_scope()
36-
return
37-
print_active_scope(target)
38-
39-
scope_type, scope = extract_from_scope(target)
33+
scope_type, scope = determine_scope(target)
34+
if not scope:
35+
return
4036

4137
if scope_type == ScopeType.CLIENT:
4238
# Get the active client manager and information

src/mcpm/commands/server_operations/add.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@
1717
from rich.prompt import Confirm
1818

1919
from mcpm.clients.client_registry import ClientRegistry
20-
from mcpm.commands.server_operations.common import client_add_server, profile_add_server
20+
from mcpm.commands.server_operations.common import client_add_server, determine_scope, profile_add_server
2121
from mcpm.profile.profile_config import ProfileConfigManager
2222
from mcpm.schemas.full_server_config import FullServerConfig
23-
from mcpm.utils.display import print_active_scope, print_no_active_scope
2423
from mcpm.utils.repository import RepositoryManager
25-
from mcpm.utils.scope import ScopeType, extract_from_scope
24+
from mcpm.utils.scope import ScopeType
2625

2726
console = Console()
2827
repo_manager = RepositoryManager()
@@ -106,15 +105,10 @@ def add(server_name, force=False, alias=None, target: str | None = None):
106105
"""
107106
config_name = alias or server_name
108107

109-
if target is None:
110-
# Get the active scope
111-
target = ClientRegistry.determine_active_scope()
112-
if not target:
113-
print_no_active_scope()
114-
return
115-
print_active_scope(target)
108+
scope_type, scope = determine_scope(target)
109+
if not scope:
110+
return
116111

117-
scope_type, scope = extract_from_scope(target)
118112
if scope_type == ScopeType.PROFILE:
119113
# Get profile
120114
profile = scope

src/mcpm/commands/server_operations/common.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,25 @@
99
console = Console()
1010

1111

12+
def determine_scope(scope: str | None) -> tuple[ScopeType | None, str | None]:
13+
if not scope:
14+
# Get the active scope
15+
scope = ClientRegistry.determine_active_scope()
16+
if not scope:
17+
print_no_active_scope()
18+
return None, None
19+
print_active_scope(scope)
20+
21+
scope_type, scope = extract_from_scope(scope)
22+
return scope_type, scope
23+
24+
1225
def determine_target(target: str) -> tuple[ScopeType | None, str | None, str | None]:
1326
scope_type, scope, server_name = parse_server(target)
1427
if not scope:
15-
active_scope = ClientRegistry.determine_active_scope()
16-
if not active_scope:
17-
print_no_active_scope()
28+
scope_type, scope = determine_scope(scope)
29+
if not scope:
1830
return None, None, None
19-
scope_type, scope = extract_from_scope(active_scope)
20-
print_active_scope(active_scope)
2131
return scope_type, scope, server_name
2232

2333

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import shlex
2+
3+
import click
4+
from rich.console import Console
5+
from rich.prompt import Confirm, Prompt
6+
7+
from mcpm.commands.server_operations.common import client_add_server, determine_scope, profile_add_server
8+
from mcpm.schemas.server_config import SSEServerConfig, STDIOServerConfig
9+
from mcpm.utils.display import print_server_config
10+
from mcpm.utils.scope import ScopeType
11+
12+
console = Console()
13+
14+
15+
@click.group()
16+
@click.help_option("-h", "--help")
17+
def import_server():
18+
"""Add server definitions manually."""
19+
pass
20+
21+
22+
@import_server.command()
23+
@click.argument("server_name", required=True)
24+
@click.option("--command", "-c", help="Executable command", required=True)
25+
@click.option("--args", "-a", multiple=True, help="Arguments for the command (can be used multiple times)")
26+
@click.option("--env", "-e", multiple=True, help="Environment variables, format: ENV=val (can be used multiple times)")
27+
@click.option("--target", "-t", help="Target client or profile")
28+
@click.option("--force", is_flag=True, help="Force reinstall if server is already installed")
29+
@click.help_option("-h", "--help")
30+
def stdio(server_name, command, args, env, target, force):
31+
"""Add a server by specifying command, args, and env variables.
32+
Examples:
33+
34+
\b
35+
mcpm import stdio <server_name> --command <command> --args <arg1> --args <arg2> --env <var1>=<value1> --env <var2>=<value2>
36+
"""
37+
scope_type, scope = determine_scope(target)
38+
if not scope:
39+
return
40+
41+
# Extract env variables
42+
env_vars = {}
43+
for item in env:
44+
if "=" in item:
45+
key, value = item.split("=", 1)
46+
env_vars[key] = value
47+
else:
48+
console.print(f"[yellow]Ignoring invalid env: {item}[/]")
49+
50+
try:
51+
# support spaces and quotes in args
52+
parsed_args = shlex.split(" ".join(args)) if args else []
53+
server_config = STDIOServerConfig(
54+
name=server_name,
55+
command=command,
56+
args=parsed_args,
57+
env=env_vars,
58+
)
59+
print_server_config(server_config)
60+
except ValueError as e:
61+
console.print(f"[bold red]Error:[/] {e}")
62+
return
63+
64+
if not Confirm.ask(f"Add this server to {scope_type} {scope}?"):
65+
return
66+
console.print(f"[green]Importing server to {scope_type} {scope}[/]")
67+
68+
if scope_type == ScopeType.CLIENT:
69+
success = client_add_server(scope, server_config, force)
70+
else:
71+
success = profile_add_server(scope, server_config, force)
72+
73+
if success:
74+
console.print(f"[bold green]Stdio server '{server_name}' added successfully to {scope_type} {scope}.")
75+
else:
76+
console.print(f"[bold red]Failed to add stdio server '{server_name}' to {scope_type} {scope}.")
77+
78+
79+
@import_server.command()
80+
@click.argument("server_name", required=True)
81+
@click.option("--url", "-u", required=True, help="Server URL")
82+
@click.option("--header", "-H", multiple=True, help="HTTP headers, format: KEY=val (can be used multiple times)")
83+
@click.option("--target", "-t", help="Target to import server to")
84+
@click.option("--force", is_flag=True, help="Force reinstall if server is already installed")
85+
@click.help_option("-h", "--help")
86+
def sse(server_name, url, header, target, force):
87+
"""Add a server by specifying a URL and headers.
88+
Examples:
89+
90+
\b
91+
mcpm import sse <server_name> --url <url> --header <key1>=<value1> --header <key2>=<value2>
92+
"""
93+
scope_type, scope = determine_scope(target)
94+
if not scope:
95+
return
96+
97+
headers = {}
98+
for item in header:
99+
if "=" in item:
100+
key, value = item.split("=", 1)
101+
headers[key] = value
102+
else:
103+
console.print(f"[yellow]Ignoring invalid header: {item}[/]")
104+
105+
try:
106+
server_config = SSEServerConfig(
107+
name=server_name,
108+
url=url,
109+
headers=headers,
110+
)
111+
print_server_config(server_config)
112+
except ValueError as e:
113+
console.print(f"[bold red]Error:[/] {e}")
114+
return
115+
116+
if not Confirm.ask(f"Add this server to {scope_type} {scope}?"):
117+
return
118+
console.print(f"[green]Importing server to {scope_type} {scope}[/]")
119+
120+
if scope_type == ScopeType.CLIENT:
121+
success = client_add_server(scope, server_config, force)
122+
else:
123+
success = profile_add_server(scope, server_config, force)
124+
125+
if success:
126+
console.print(f"[bold green]SSE server '{server_name}' added successfully to {scope_type} {scope}.")
127+
else:
128+
console.print(f"[bold red]Failed to add SSE server '{server_name}' to {scope_type} {scope}.")
129+
130+
131+
@import_server.command()
132+
@click.option("--target", "-t", help="Target to import server to")
133+
@click.help_option("-h", "--help")
134+
def interact(target: str | None = None):
135+
"""Add a server by manually configuring it interactively."""
136+
scope_type, scope = determine_scope(target)
137+
if not scope:
138+
return
139+
140+
server_name = Prompt.ask("Enter server name")
141+
if not server_name:
142+
console.print("[red]Server name cannot be empty.[/]")
143+
return
144+
145+
config_type = Prompt.ask("Select server type", choices=["stdio", "sse"], default="stdio")
146+
147+
if config_type == "stdio":
148+
command = Prompt.ask("Enter command (executable)")
149+
args = Prompt.ask("Enter arguments (space-separated, optional)", default="")
150+
env_input = Prompt.ask("Enter env variables (format: KEY=VAL, comma-separated, optional)", default="")
151+
env = {}
152+
if env_input.strip():
153+
for pair in env_input.split(","):
154+
if "=" in pair:
155+
k, v = pair.split("=", 1)
156+
env[k.strip()] = v.strip()
157+
try:
158+
# support spaces and quotes in args
159+
parsed_args = shlex.split(args) if args.strip() else []
160+
server_config = STDIOServerConfig(
161+
name=server_name,
162+
command=command,
163+
args=parsed_args,
164+
env=env,
165+
)
166+
except ValueError as e:
167+
console.print(f"[bold red]Error:[/] {e}")
168+
return
169+
elif config_type == "sse":
170+
url = Prompt.ask("Enter SSE server URL")
171+
headers_input = Prompt.ask("Enter HTTP headers (format: KEY=VAL, comma-separated, optional)", default="")
172+
headers = {}
173+
if headers_input.strip():
174+
for pair in headers_input.split(","):
175+
if "=" in pair:
176+
k, v = pair.split("=", 1)
177+
headers[k.strip()] = v.strip()
178+
try:
179+
server_config = SSEServerConfig(
180+
name=server_name,
181+
url=url,
182+
headers=headers,
183+
)
184+
except ValueError as e:
185+
console.print(f"[bold red]Error:[/] {e}")
186+
return
187+
else:
188+
console.print(f"[red]Unknown server type: {config_type}[/]")
189+
return
190+
191+
print_server_config(server_config)
192+
if not Confirm.ask(f"Add this server to {scope_type} {scope}?"):
193+
return
194+
console.print(f"[green]Importing server to {scope_type} {scope}[/]")
195+
196+
if scope_type == ScopeType.CLIENT:
197+
success = client_add_server(scope, server_config, False)
198+
else:
199+
success = profile_add_server(scope, server_config, False)
200+
201+
if success:
202+
console.print(
203+
f"[bold green]{config_type.upper()} server '{server_name}' added successfully to {scope_type} {scope}."
204+
)
205+
else:
206+
console.print(f"[bold red]Failed to add {config_type.upper()} server '{server_name}' to {scope_type} {scope}.")

0 commit comments

Comments
 (0)