Skip to content

Commit 39a8515

Browse files
committed
profile share working
1 parent 604320b commit 39a8515

File tree

17 files changed

+1595
-638
lines changed

17 files changed

+1595
-638
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Understanding CustomServerConfig in MCPM
2+
3+
## What is CustomServerConfig?
4+
5+
`CustomServerConfig` is a flexible server configuration type in MCPM that allows for arbitrary, non-standard server configurations. It's designed to support future extensibility and edge cases that don't fit into the standard STDIO or Remote server models.
6+
7+
## Structure
8+
9+
```python
10+
class CustomServerConfig(BaseServerConfig):
11+
config: Dict[str, Any]
12+
```
13+
14+
- Inherits from `BaseServerConfig` (provides `name` and `profile_tags`)
15+
- Has a single `config` field that can contain any dictionary structure
16+
17+
## Purpose and Use Cases
18+
19+
### 1. **Future-Proofing**
20+
CustomServerConfig allows MCPM to support new server types without changing the core schema. For example:
21+
- WebSocket-based MCP servers
22+
- Custom transport protocols
23+
- Proprietary server implementations
24+
25+
### 2. **Client-Specific Configurations**
26+
Different MCP clients might have unique configuration requirements:
27+
```python
28+
# Example: A hypothetical WebSocket server
29+
CustomServerConfig(
30+
name="websocket-server",
31+
config={
32+
"url": "wss://example.com/mcp",
33+
"transport": "websocket",
34+
"reconnect": True,
35+
"ping_interval": 30
36+
}
37+
)
38+
```
39+
40+
### 3. **Built-in or Special Servers**
41+
In the Goose client integration, CustomServerConfig is used for "builtin" type servers:
42+
```python
43+
# From goose.py client manager
44+
elif isinstance(server_config, CustomServerConfig):
45+
result = dict(server_config.config)
46+
result["type"] = "builtin"
47+
```
48+
49+
## How It Works in the FastMCP Proxy
50+
51+
When the FastMCP proxy encounters a CustomServerConfig, it passes the entire `config` dictionary directly to FastMCP:
52+
53+
```python
54+
elif isinstance(server, CustomServerConfig):
55+
# CustomServerConfig - pass through the custom config
56+
proxy_config["mcpServers"][server.name] = server.config
57+
```
58+
59+
This allows FastMCP to handle any configuration format it supports, including:
60+
- Custom transports
61+
- Special authentication methods
62+
- Non-standard connection parameters
63+
64+
## Example Usage Scenarios
65+
66+
### 1. WebSocket Server
67+
```yaml
68+
name: realtime-data
69+
config:
70+
url: wss://data.example.com/mcp
71+
transport: websocket
72+
auth_token: ${WEBSOCKET_TOKEN}
73+
reconnect_delay: 5000
74+
```
75+
76+
### 2. Plugin-Based Server
77+
```yaml
78+
name: plugin-server
79+
config:
80+
type: plugin
81+
module: mcp_plugins.analytics
82+
class: AnalyticsServer
83+
options:
84+
cache_size: 1000
85+
refresh_rate: 60
86+
```
87+
88+
### 3. Embedded Server
89+
```yaml
90+
name: embedded-llm
91+
config:
92+
type: embedded
93+
model_path: /models/local-llm
94+
gpu_layers: 32
95+
context_size: 4096
96+
```
97+
98+
## Current Implementation Status
99+
100+
As of now, CustomServerConfig is:
101+
- ✅ Defined in the schema
102+
- ✅ Supported by the FastMCP proxy
103+
- ✅ Handled by client managers (like Goose)
104+
- ⚠️ Not yet used in practice by MCPM commands
105+
106+
## Why It Exists
107+
108+
1. **Extensibility**: Allows MCPM to support new server types without breaking changes
109+
2. **Flexibility**: Clients can define their own server configuration formats
110+
3. **Forward Compatibility**: Future MCP transport types can be supported
111+
4. **Special Cases**: Handles edge cases that don't fit STDIO or HTTP/SSE models
112+
113+
## Integration with FastMCP
114+
115+
FastMCP's proxy system is designed to handle arbitrary configurations. When it receives a CustomServerConfig, it:
116+
1. Extracts the `config` dictionary
117+
2. Passes it directly to FastMCP's configuration system
118+
3. FastMCP interprets the configuration based on its own rules
119+
120+
This design allows MCPM to be transport-agnostic while still providing structured configuration for known transport types (STDIO and Remote).
121+
122+
## Future Possibilities
123+
124+
CustomServerConfig opens the door for:
125+
- gRPC-based MCP servers
126+
- Message queue-based servers (RabbitMQ, Kafka)
127+
- In-process servers (Python modules loaded directly)
128+
- Cloud-native integrations (Lambda, Cloud Functions)
129+
- Custom authentication schemes
130+
- Load-balanced server pools
131+
132+
The key insight is that CustomServerConfig is MCPM's escape hatch for innovation - it ensures the tool can adapt to new MCP server types without requiring schema changes.

examples/proxy_transport_types.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# FastMCP Proxy with Multiple Transport Types
2+
3+
The MCPM FastMCP proxy now supports aggregating servers with different transport types (stdio, HTTP, SSE) into a single unified interface.
4+
5+
## Supported Server Types
6+
7+
### 1. STDIO Servers (Local Command-based)
8+
```yaml
9+
name: local-python-server
10+
command: python
11+
args: ["-m", "my_mcp_server"]
12+
env:
13+
API_KEY: ${MY_API_KEY}
14+
```
15+
16+
### 2. Remote HTTP/SSE Servers
17+
```yaml
18+
name: remote-api-server
19+
url: https://api.example.com/mcp
20+
headers:
21+
Authorization: Bearer ${TOKEN}
22+
```
23+
24+
### 3. Custom Server Configurations (Client-Specific)
25+
```yaml
26+
name: custom-websocket-server
27+
config:
28+
url: wss://ws.example.com/mcp
29+
transport: websocket
30+
custom_field: value
31+
```
32+
33+
**Note**: CustomServerConfig is used for parsing non-standard MCP configs from client configuration files (like Claude Desktop, Goose, etc.) and is **not processed by MCPM's proxy system**. These are client-specific configurations that don't go through the proxy.
34+
35+
## Example: Mixed Profile
36+
37+
You can create a profile that combines servers with different transports:
38+
39+
```bash
40+
# Add a local stdio server
41+
mcpm add mcp-server-time --profile mixed-demo
42+
43+
# Add a remote HTTP server (hypothetical)
44+
mcpm client add --global --server remote-weather --url https://weather-api.com/mcp
45+
46+
# Run the profile - FastMCP proxy handles all transport types
47+
mcpm profile run mixed-demo
48+
49+
# Or run in HTTP mode for sharing
50+
mcpm profile run --http mixed-demo
51+
```
52+
53+
## How It Works
54+
55+
1. **STDIO Servers**: The proxy runs the command directly with the specified arguments and environment
56+
2. **HTTP/SSE Servers**: The proxy connects to the remote URL, forwarding headers as needed
57+
3. **Custom Servers**: These are skipped by the proxy system (client-specific configurations)
58+
59+
## Benefits
60+
61+
- **Unified Interface**: Access all servers through a single endpoint
62+
- **Transport Bridging**: Expose HTTP-only servers via stdio or vice versa
63+
- **Authentication**: Add MCPM authentication layer to any server type
64+
- **Monitoring**: Track usage across all server types uniformly
65+
66+
## FastMCP Proxy Configuration
67+
68+
When MCPM creates the FastMCP proxy, it generates a configuration like:
69+
70+
```json
71+
{
72+
"mcpServers": {
73+
"local-server": {
74+
"command": ["python", "-m", "server"],
75+
"env": {"KEY": "value"}
76+
},
77+
"remote-server": {
78+
"url": "https://api.example.com/mcp",
79+
"transport": "http",
80+
"headers": {"Authorization": "Bearer token"}
81+
}
82+
}
83+
}
84+
```
85+
86+
**Note**: CustomServerConfig entries are not included in the proxy configuration as they are client-specific and not processed by MCPM's proxy system.
87+
88+
FastMCP handles the complexity of connecting to each supported server type and presents a unified MCP interface.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"requests>=2.28.0",
2626
"pydantic>=2.5.1",
2727
"mcp>=1.8.0",
28+
"fastmcp>=2.0.0",
2829
"ruamel-yaml>=0.18.10",
2930
"watchfiles>=1.0.4",
3031
"duckdb>=1.2.2",

src/mcpm/commands/profile/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .create import create_profile
66
from .edit import edit_profile
7+
from .inspect import inspect_profile
78
from .list import list_profiles
89
from .remove import remove_profile
910
from .run import run
@@ -21,6 +22,7 @@ def profile():
2122
profile.add_command(list_profiles)
2223
profile.add_command(create_profile)
2324
profile.add_command(edit_profile)
25+
profile.add_command(inspect_profile)
2426
profile.add_command(share_profile)
2527
profile.add_command(remove_profile)
2628
profile.add_command(run)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Profile inspect command."""
2+
3+
import shlex
4+
import subprocess
5+
import sys
6+
7+
import click
8+
from rich.console import Console
9+
from rich.panel import Panel
10+
11+
from mcpm.profile.profile_config import ProfileConfigManager
12+
from mcpm.utils.platform import NPX_CMD
13+
14+
console = Console()
15+
profile_config_manager = ProfileConfigManager()
16+
17+
18+
def build_profile_inspector_command(profile_name):
19+
"""Build the inspector command using mcpm profile run."""
20+
# Use mcpm profile run to start the FastMCP proxy - don't reinvent the wheel!
21+
mcpm_profile_run_cmd = f"mcpm profile run {shlex.quote(profile_name)}"
22+
23+
# Build inspector command that uses mcpm profile run
24+
inspector_cmd = f"{NPX_CMD} @modelcontextprotocol/inspector {mcpm_profile_run_cmd}"
25+
return inspector_cmd
26+
27+
28+
@click.command(name="inspect")
29+
@click.argument("profile_name")
30+
@click.help_option("-h", "--help")
31+
def inspect_profile(profile_name):
32+
"""Launch MCP Inspector to test and debug all servers in a profile.
33+
34+
Creates a FastMCP proxy that aggregates all servers in the specified profile
35+
and launches the MCP Inspector to interact with the combined capabilities.
36+
37+
Examples:
38+
mcpm profile inspect web-dev # Inspect all servers in web-dev profile
39+
mcpm profile inspect ai # Inspect all servers in ai profile
40+
mcpm profile inspect data # Inspect all servers in data profile
41+
"""
42+
# Validate profile name
43+
if not profile_name or not profile_name.strip():
44+
console.print("[red]Error: Profile name cannot be empty[/]")
45+
sys.exit(1)
46+
47+
profile_name = profile_name.strip()
48+
49+
# Show header
50+
console.print(
51+
Panel.fit(
52+
f"[bold green]MCPM Profile Inspector[/]\\nInspecting profile: [cyan]{profile_name}[/]",
53+
border_style="cyan"
54+
)
55+
)
56+
57+
# Check if profile exists
58+
try:
59+
profile_servers = profile_config_manager.get_profile(profile_name)
60+
if profile_servers is None:
61+
console.print(f"[red]Error: Profile '[bold]{profile_name}[/]' not found[/]")
62+
console.print()
63+
console.print("[yellow]Available options:[/]")
64+
console.print(" • Run 'mcpm profile ls' to see available profiles")
65+
console.print(" • Run 'mcpm profile create {name}' to create a profile")
66+
sys.exit(1)
67+
except Exception as e:
68+
console.print(f"[red]Error accessing profile '{profile_name}': {e}[/]")
69+
sys.exit(1)
70+
71+
if not profile_servers:
72+
console.print(f"[yellow]Profile '[bold]{profile_name}[/]' has no servers configured[/]")
73+
console.print()
74+
console.print("[dim]Add servers to this profile with:[/]")
75+
console.print(f" mcpm profile edit {profile_name}")
76+
sys.exit(1)
77+
78+
# Show profile info
79+
server_count = len(profile_servers)
80+
console.print(f"[dim]Profile contains {server_count} server(s):[/]")
81+
for server_config in profile_servers:
82+
console.print(f" • [cyan]{server_config.name}[/]")
83+
84+
console.print(f"\\n[bold]Starting Inspector for profile '[cyan]{profile_name}[/]'[/]")
85+
console.print("The Inspector will show aggregated capabilities from all servers in the profile.")
86+
console.print("The Inspector UI will open in your web browser.")
87+
88+
# Build inspector command using mcpm profile run
89+
inspector_cmd = build_profile_inspector_command(profile_name)
90+
91+
try:
92+
console.print("[cyan]Starting MCPM Profile Inspector...[/]")
93+
console.print("The Inspector UI will open in your web browser.")
94+
console.print("[yellow]Press Ctrl+C to stop the Inspector.[/]")
95+
96+
# Split the command into components for subprocess
97+
cmd_parts = shlex.split(inspector_cmd)
98+
99+
try:
100+
console.print(f"[dim]Executing: {inspector_cmd}[/]")
101+
console.print("[bold green]Starting MCPM Profile Inspector...[/]")
102+
console.print("[cyan]Press Ctrl+C to exit[/]")
103+
sys.stdout.flush()
104+
105+
# Execute the command with direct terminal access
106+
returncode = subprocess.call(cmd_parts)
107+
108+
except KeyboardInterrupt:
109+
console.print("\\n[bold yellow]Inspector process terminated by keyboard interrupt.[/]")
110+
returncode = 130
111+
112+
# Check exit code
113+
if returncode == 0:
114+
console.print("[bold green]Inspector process completed successfully.[/]")
115+
elif returncode in (130, -2):
116+
console.print("[bold yellow]Inspector process was terminated.[/]")
117+
else:
118+
console.print(f"[bold red]Inspector process exited with code {returncode}[/]")
119+
120+
sys.exit(returncode)
121+
122+
except FileNotFoundError:
123+
console.print("[bold red]Error:[/] Could not find npx. Please make sure Node.js is installed.")
124+
console.print("Install Node.js from https://nodejs.org/")
125+
sys.exit(1)
126+
except PermissionError:
127+
console.print("[bold red]Error:[/] Permission denied while trying to execute the command.")
128+
sys.exit(1)
129+
except Exception as e:
130+
console.print(f"[bold red]Error launching Profile Inspector:[/] {str(e)}")
131+
sys.exit(1)

0 commit comments

Comments
 (0)