meshagent-cli 0.22.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of meshagent-cli might be problematic. Click here for more details.
- meshagent/cli/__init__.py +3 -0
- meshagent/cli/agent.py +273 -0
- meshagent/cli/api_keys.py +102 -0
- meshagent/cli/async_typer.py +79 -0
- meshagent/cli/auth.py +30 -0
- meshagent/cli/auth_async.py +295 -0
- meshagent/cli/call.py +215 -0
- meshagent/cli/chatbot.py +1983 -0
- meshagent/cli/cli.py +187 -0
- meshagent/cli/cli_mcp.py +408 -0
- meshagent/cli/cli_secrets.py +414 -0
- meshagent/cli/common_options.py +47 -0
- meshagent/cli/containers.py +725 -0
- meshagent/cli/database.py +997 -0
- meshagent/cli/developer.py +70 -0
- meshagent/cli/exec.py +397 -0
- meshagent/cli/helper.py +236 -0
- meshagent/cli/helpers.py +185 -0
- meshagent/cli/host.py +41 -0
- meshagent/cli/mailbot.py +1295 -0
- meshagent/cli/mailboxes.py +223 -0
- meshagent/cli/meeting_transcriber.py +138 -0
- meshagent/cli/messaging.py +157 -0
- meshagent/cli/multi.py +357 -0
- meshagent/cli/oauth2.py +341 -0
- meshagent/cli/participant_token.py +63 -0
- meshagent/cli/port.py +70 -0
- meshagent/cli/projects.py +105 -0
- meshagent/cli/queue.py +91 -0
- meshagent/cli/room.py +26 -0
- meshagent/cli/rooms.py +214 -0
- meshagent/cli/services.py +722 -0
- meshagent/cli/sessions.py +26 -0
- meshagent/cli/storage.py +813 -0
- meshagent/cli/sync.py +434 -0
- meshagent/cli/task_runner.py +1317 -0
- meshagent/cli/version.py +1 -0
- meshagent/cli/voicebot.py +624 -0
- meshagent/cli/webhook.py +100 -0
- meshagent/cli/worker.py +1403 -0
- meshagent_cli-0.22.2.dist-info/METADATA +49 -0
- meshagent_cli-0.22.2.dist-info/RECORD +45 -0
- meshagent_cli-0.22.2.dist-info/WHEEL +5 -0
- meshagent_cli-0.22.2.dist-info/entry_points.txt +2 -0
- meshagent_cli-0.22.2.dist-info/top_level.txt +1 -0
meshagent/cli/cli.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from meshagent.cli import async_typer
|
|
5
|
+
|
|
6
|
+
from meshagent.cli import multi
|
|
7
|
+
|
|
8
|
+
from meshagent.cli import auth
|
|
9
|
+
from meshagent.cli import api_keys
|
|
10
|
+
from meshagent.cli import projects
|
|
11
|
+
from meshagent.cli import sessions
|
|
12
|
+
from meshagent.cli import participant_token
|
|
13
|
+
from meshagent.cli import webhook
|
|
14
|
+
from meshagent.cli import services
|
|
15
|
+
from meshagent.cli import mailboxes
|
|
16
|
+
|
|
17
|
+
from meshagent.cli import call
|
|
18
|
+
from meshagent.cli import cli_mcp
|
|
19
|
+
from meshagent.cli import chatbot
|
|
20
|
+
from meshagent.cli import voicebot
|
|
21
|
+
from meshagent.cli import mailbot
|
|
22
|
+
from meshagent.cli import worker
|
|
23
|
+
from meshagent.cli import task_runner
|
|
24
|
+
from meshagent.cli import cli_secrets
|
|
25
|
+
from meshagent.cli import helpers
|
|
26
|
+
from meshagent.cli import meeting_transcriber
|
|
27
|
+
from meshagent.cli import rooms
|
|
28
|
+
from meshagent.cli import room
|
|
29
|
+
from meshagent.cli import port
|
|
30
|
+
from meshagent.cli.exec import register as register_exec
|
|
31
|
+
from meshagent.cli.version import __version__
|
|
32
|
+
from meshagent.cli.helper import get_active_api_key
|
|
33
|
+
from meshagent.otel import otel_config
|
|
34
|
+
|
|
35
|
+
from art import tprint
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
otel_config(service_name="meshagent-cli")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Turn down OpenAI logs, they are a bit noisy
|
|
47
|
+
logging.getLogger("openai").setLevel(logging.ERROR)
|
|
48
|
+
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
49
|
+
|
|
50
|
+
app = async_typer.AsyncTyper(no_args_is_help=True, name="meshagent")
|
|
51
|
+
app.add_typer(call.app, name="call")
|
|
52
|
+
app.add_typer(auth.app, name="auth")
|
|
53
|
+
app.add_typer(projects.app, name="project")
|
|
54
|
+
app.add_typer(api_keys.app, name="api-key")
|
|
55
|
+
app.add_typer(sessions.app, name="session")
|
|
56
|
+
app.add_typer(participant_token.app, name="token")
|
|
57
|
+
app.add_typer(webhook.app, name="webhook")
|
|
58
|
+
app.add_typer(services.app, name="service")
|
|
59
|
+
app.add_typer(cli_mcp.app, name="mcp")
|
|
60
|
+
app.add_typer(cli_secrets.app, name="secrets")
|
|
61
|
+
app.add_typer(helpers.app, name="helpers")
|
|
62
|
+
app.add_typer(rooms.app, name="rooms")
|
|
63
|
+
app.add_typer(mailboxes.app, name="mailbox")
|
|
64
|
+
app.add_typer(meeting_transcriber.app, name="meeting-transcriber")
|
|
65
|
+
app.add_typer(port.app, name="port")
|
|
66
|
+
|
|
67
|
+
app.add_typer(multi.app, name="multi")
|
|
68
|
+
app.add_typer(voicebot.app, name="voicebot")
|
|
69
|
+
app.add_typer(chatbot.app, name="chatbot")
|
|
70
|
+
app.add_typer(mailbot.app, name="mailbot")
|
|
71
|
+
app.add_typer(task_runner.app, name="task-runner")
|
|
72
|
+
app.add_typer(worker.app, name="worker")
|
|
73
|
+
|
|
74
|
+
app.add_typer(room.app, name="room")
|
|
75
|
+
|
|
76
|
+
register_exec(app)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run_async(coro):
|
|
80
|
+
asyncio.run(coro)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def detect_shell() -> str:
|
|
84
|
+
"""
|
|
85
|
+
Best-effort detection of the *current* interactive shell.
|
|
86
|
+
|
|
87
|
+
Order of preference
|
|
88
|
+
1. Explicit --shell argument (handled by Typer)
|
|
89
|
+
2. Per-shell env vars set by the running shell
|
|
90
|
+
• BASH_VERSION / ZSH_VERSION / FISH_VERSION
|
|
91
|
+
3. $SHELL on POSIX (user’s login shell – still correct >90 % of the time)
|
|
92
|
+
4. Parent process on Windows (COMSPEC → cmd / powershell)
|
|
93
|
+
5. Safe default: 'bash'
|
|
94
|
+
"""
|
|
95
|
+
# Per-shell version variables (works even if login shell ≠ current shell)
|
|
96
|
+
for var, name in (
|
|
97
|
+
("ZSH_VERSION", "zsh"),
|
|
98
|
+
("BASH_VERSION", "bash"),
|
|
99
|
+
("FISH_VERSION", "fish"),
|
|
100
|
+
):
|
|
101
|
+
if var in os.environ:
|
|
102
|
+
return name
|
|
103
|
+
|
|
104
|
+
# POSIX fallback: login shell path
|
|
105
|
+
sh = os.environ.get("SHELL")
|
|
106
|
+
if sh:
|
|
107
|
+
return Path(sh).name.lower()
|
|
108
|
+
|
|
109
|
+
# Windows heuristics
|
|
110
|
+
if sys.platform == "win32":
|
|
111
|
+
comspec = Path(os.environ.get("COMSPEC", "")).name.lower()
|
|
112
|
+
if "powershell" in comspec:
|
|
113
|
+
return "powershell"
|
|
114
|
+
if "cmd" in comspec:
|
|
115
|
+
return "cmd"
|
|
116
|
+
return "powershell" # sensible default on modern Windows
|
|
117
|
+
|
|
118
|
+
# Last-ditch default
|
|
119
|
+
return "bash"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _bash_like(name: str, value: str, unset: bool) -> str:
|
|
123
|
+
return f"unset {name}" if unset else f'export {name}="{value}"'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _fish(name: str, value: str, unset: bool) -> str:
|
|
127
|
+
return f"set -e {name}" if unset else f'set -gx {name} "{value}"'
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _powershell(name: str, value: str, unset: bool) -> str:
|
|
131
|
+
return f"Remove-Item Env:{name}" if unset else f'$Env:{name}="{value}"'
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _cmd(name: str, value: str, unset: bool) -> str:
|
|
135
|
+
return f"set {name}=" if unset else f"set {name}={value}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
SHELL_RENDERERS = {
|
|
139
|
+
"bash": _bash_like,
|
|
140
|
+
"zsh": _bash_like,
|
|
141
|
+
"fish": _fish,
|
|
142
|
+
"powershell": _powershell,
|
|
143
|
+
"cmd": _cmd,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command(
|
|
148
|
+
"version",
|
|
149
|
+
help="Print the version",
|
|
150
|
+
)
|
|
151
|
+
def version():
|
|
152
|
+
print(__version__)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command("setup")
|
|
156
|
+
def setup_command():
|
|
157
|
+
"""Perform initial login and project/api key activation."""
|
|
158
|
+
|
|
159
|
+
async def runner():
|
|
160
|
+
print("\n", flush=True)
|
|
161
|
+
tprint("MeshAgent", "tarty10")
|
|
162
|
+
print("\n", flush=True)
|
|
163
|
+
await auth.login()
|
|
164
|
+
print("Activate a project...")
|
|
165
|
+
project_id = await projects.activate(None, interactive=True)
|
|
166
|
+
if project_id is None:
|
|
167
|
+
print("You have choosen to not activate a project. Exiting.")
|
|
168
|
+
if (
|
|
169
|
+
project_id is not None
|
|
170
|
+
and await get_active_api_key(project_id=project_id) is None
|
|
171
|
+
):
|
|
172
|
+
if typer.confirm(
|
|
173
|
+
"You do not have an active api key for this project. Would you like to create and activate a new api key?",
|
|
174
|
+
default=True,
|
|
175
|
+
):
|
|
176
|
+
name = typer.prompt(
|
|
177
|
+
"Enter a name for your API Key (must be a unique name):"
|
|
178
|
+
)
|
|
179
|
+
await api_keys.create(
|
|
180
|
+
project_id=None, activate=True, silent=True, name=name
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
_run_async(runner())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
app()
|
meshagent/cli/cli_mcp.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich import print
|
|
3
|
+
from typing import Annotated, Optional, List
|
|
4
|
+
from meshagent.cli.common_options import ProjectIdOption, RoomOption
|
|
5
|
+
|
|
6
|
+
from meshagent.api.helpers import meshagent_base_url, websocket_room_url
|
|
7
|
+
from meshagent.api import RoomClient, WebSocketClientProtocol, RoomException
|
|
8
|
+
from meshagent.cli import async_typer
|
|
9
|
+
from meshagent.cli.helper import (
|
|
10
|
+
get_client,
|
|
11
|
+
resolve_project_id,
|
|
12
|
+
resolve_room,
|
|
13
|
+
resolve_key,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from meshagent.tools.hosting import RemoteToolkit
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from meshagent.api.services import ServiceHost
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
import shlex
|
|
23
|
+
|
|
24
|
+
from meshagent.api import ParticipantToken, ApiScope
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _kv_to_dict(pairs: List[str]) -> dict[str, str]:
|
|
28
|
+
"""Convert ["A=1","B=2"] → {"A":"1","B":"2"}."""
|
|
29
|
+
out: dict[str, str] = {}
|
|
30
|
+
for p in pairs:
|
|
31
|
+
if "=" not in p:
|
|
32
|
+
raise typer.BadParameter(f"'{p}' must be KEY=VALUE")
|
|
33
|
+
k, v = p.split("=", 1)
|
|
34
|
+
out[k] = v
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
app = async_typer.AsyncTyper(help="Bridge MCP servers into MeshAgent rooms")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.async_command(
|
|
42
|
+
"sse", help="Connect an MCP server over SSE and register it as a toolkit"
|
|
43
|
+
)
|
|
44
|
+
async def sse(
|
|
45
|
+
*,
|
|
46
|
+
project_id: ProjectIdOption,
|
|
47
|
+
room: RoomOption,
|
|
48
|
+
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
49
|
+
role: str = "tool",
|
|
50
|
+
url: Annotated[str, typer.Option(..., help="SSE URL for the MCP server")],
|
|
51
|
+
toolkit_name: Annotated[
|
|
52
|
+
Optional[str],
|
|
53
|
+
typer.Option(help="Toolkit name to register in the room (default: mcp)"),
|
|
54
|
+
] = None,
|
|
55
|
+
key: Annotated[
|
|
56
|
+
str,
|
|
57
|
+
typer.Option("--key", help="an api key to sign the token with"),
|
|
58
|
+
] = None,
|
|
59
|
+
):
|
|
60
|
+
"""Connect an MCP server over SSE and expose it as a room toolkit."""
|
|
61
|
+
|
|
62
|
+
from mcp.client.session import ClientSession
|
|
63
|
+
from mcp.client.sse import sse_client
|
|
64
|
+
|
|
65
|
+
from meshagent.mcp import MCPToolkit
|
|
66
|
+
|
|
67
|
+
key = await resolve_key(project_id=project_id, key=key)
|
|
68
|
+
|
|
69
|
+
if toolkit_name is None:
|
|
70
|
+
toolkit_name = "mcp"
|
|
71
|
+
|
|
72
|
+
account_client = await get_client()
|
|
73
|
+
try:
|
|
74
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
75
|
+
room = resolve_room(room)
|
|
76
|
+
|
|
77
|
+
token = ParticipantToken(
|
|
78
|
+
name=name,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
token.add_api_grant(ApiScope.agent_default())
|
|
82
|
+
|
|
83
|
+
token.add_role_grant(role=role)
|
|
84
|
+
token.add_room_grant(room)
|
|
85
|
+
|
|
86
|
+
jwt = token.to_jwt(api_key=key)
|
|
87
|
+
|
|
88
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
89
|
+
async with RoomClient(
|
|
90
|
+
protocol=WebSocketClientProtocol(
|
|
91
|
+
url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
|
|
92
|
+
token=jwt,
|
|
93
|
+
)
|
|
94
|
+
) as client:
|
|
95
|
+
async with sse_client(url) as (read_stream, write_stream):
|
|
96
|
+
async with ClientSession(
|
|
97
|
+
read_stream=read_stream, write_stream=write_stream
|
|
98
|
+
) as session:
|
|
99
|
+
mcp_tools_response = await session.list_tools()
|
|
100
|
+
|
|
101
|
+
toolkit = MCPToolkit(
|
|
102
|
+
name=toolkit_name,
|
|
103
|
+
session=session,
|
|
104
|
+
tools=mcp_tools_response.tools,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
remote_toolkit = RemoteToolkit(
|
|
108
|
+
name=toolkit.name,
|
|
109
|
+
tools=toolkit.tools,
|
|
110
|
+
title=toolkit.title,
|
|
111
|
+
description=toolkit.description,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
await remote_toolkit.start(room=client)
|
|
115
|
+
try:
|
|
116
|
+
await client.protocol.wait_for_close()
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
await remote_toolkit.stop()
|
|
119
|
+
|
|
120
|
+
except RoomException as e:
|
|
121
|
+
print(f"[red]{e}[/red]")
|
|
122
|
+
finally:
|
|
123
|
+
await account_client.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.async_command(
|
|
127
|
+
"stdio", help="Run an MCP server over stdio and register it as a toolkit"
|
|
128
|
+
)
|
|
129
|
+
async def stdio(
|
|
130
|
+
*,
|
|
131
|
+
project_id: ProjectIdOption,
|
|
132
|
+
room: RoomOption,
|
|
133
|
+
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
134
|
+
role: str = "tool",
|
|
135
|
+
command: Annotated[
|
|
136
|
+
str,
|
|
137
|
+
typer.Option(
|
|
138
|
+
..., help="Command to start an MCP server over stdio (quoted string)"
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
toolkit_name: Annotated[
|
|
142
|
+
Optional[str],
|
|
143
|
+
typer.Option(help="Toolkit name to register in the room (default: mcp)"),
|
|
144
|
+
] = None,
|
|
145
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
146
|
+
key: Annotated[
|
|
147
|
+
str,
|
|
148
|
+
typer.Option("--key", help="an api key to sign the token with"),
|
|
149
|
+
] = None,
|
|
150
|
+
):
|
|
151
|
+
"""Run an MCP server over stdio and expose it as a room toolkit."""
|
|
152
|
+
|
|
153
|
+
from mcp.client.session import ClientSession
|
|
154
|
+
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
155
|
+
|
|
156
|
+
from meshagent.mcp import MCPToolkit
|
|
157
|
+
|
|
158
|
+
key = await resolve_key(project_id=project_id, key=key)
|
|
159
|
+
|
|
160
|
+
if toolkit_name is None:
|
|
161
|
+
toolkit_name = "mcp"
|
|
162
|
+
|
|
163
|
+
account_client = await get_client()
|
|
164
|
+
try:
|
|
165
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
166
|
+
room = resolve_room(room)
|
|
167
|
+
|
|
168
|
+
token = ParticipantToken(
|
|
169
|
+
name=name,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
token.add_api_grant(ApiScope.agent_default())
|
|
173
|
+
|
|
174
|
+
token.add_role_grant(role=role)
|
|
175
|
+
token.add_room_grant(room)
|
|
176
|
+
|
|
177
|
+
jwt = token.to_jwt(api_key=key)
|
|
178
|
+
|
|
179
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
180
|
+
async with RoomClient(
|
|
181
|
+
protocol=WebSocketClientProtocol(
|
|
182
|
+
url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
|
|
183
|
+
token=jwt,
|
|
184
|
+
)
|
|
185
|
+
) as client:
|
|
186
|
+
parsed_command = shlex.split(command)
|
|
187
|
+
|
|
188
|
+
async with (
|
|
189
|
+
stdio_client(
|
|
190
|
+
StdioServerParameters(
|
|
191
|
+
command=parsed_command[0], # Executable
|
|
192
|
+
args=parsed_command[1:], # Optional command line arguments
|
|
193
|
+
env=_kv_to_dict(env), # Optional environment variables
|
|
194
|
+
)
|
|
195
|
+
) as (read_stream, write_stream)
|
|
196
|
+
):
|
|
197
|
+
async with ClientSession(
|
|
198
|
+
read_stream=read_stream, write_stream=write_stream
|
|
199
|
+
) as session:
|
|
200
|
+
mcp_tools_response = await session.list_tools()
|
|
201
|
+
|
|
202
|
+
toolkit = MCPToolkit(
|
|
203
|
+
name=toolkit_name,
|
|
204
|
+
session=session,
|
|
205
|
+
tools=mcp_tools_response.tools,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
remote_toolkit = RemoteToolkit(
|
|
209
|
+
name=toolkit.name,
|
|
210
|
+
tools=toolkit.tools,
|
|
211
|
+
title=toolkit.title,
|
|
212
|
+
description=toolkit.description,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
await remote_toolkit.start(room=client)
|
|
216
|
+
try:
|
|
217
|
+
await client.protocol.wait_for_close()
|
|
218
|
+
except KeyboardInterrupt:
|
|
219
|
+
await remote_toolkit.stop()
|
|
220
|
+
|
|
221
|
+
except RoomException as e:
|
|
222
|
+
print(f"[red]{e}[/red]")
|
|
223
|
+
finally:
|
|
224
|
+
await account_client.close()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@app.async_command("http-proxy", help="Expose a stdio MCP server over streamable HTTP")
|
|
228
|
+
async def stdio_host(
|
|
229
|
+
*,
|
|
230
|
+
command: Annotated[
|
|
231
|
+
str,
|
|
232
|
+
typer.Option(..., help="Command to start the MCP server (stdio transport)"),
|
|
233
|
+
],
|
|
234
|
+
host: Annotated[
|
|
235
|
+
Optional[str], typer.Option(help="Host to bind the proxy server on")
|
|
236
|
+
] = None,
|
|
237
|
+
port: Annotated[
|
|
238
|
+
Optional[int], typer.Option(help="Port to bind the proxy server on")
|
|
239
|
+
] = None,
|
|
240
|
+
path: Annotated[
|
|
241
|
+
Optional[str],
|
|
242
|
+
typer.Option(help="HTTP path to mount the proxy server at"),
|
|
243
|
+
] = None,
|
|
244
|
+
name: Annotated[
|
|
245
|
+
Optional[str], typer.Option(help="Display name for the proxy server")
|
|
246
|
+
] = None,
|
|
247
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
248
|
+
):
|
|
249
|
+
"""Expose a stdio-based MCP server over streamable HTTP."""
|
|
250
|
+
|
|
251
|
+
from fastmcp import FastMCP, Client
|
|
252
|
+
from fastmcp.client.transports import StdioTransport
|
|
253
|
+
|
|
254
|
+
parsed_command = shlex.split(command)
|
|
255
|
+
|
|
256
|
+
# Create a client that connects to the original server
|
|
257
|
+
proxy_client = Client(
|
|
258
|
+
transport=StdioTransport(
|
|
259
|
+
parsed_command[0], parsed_command[1:], _kv_to_dict(env)
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if name is None:
|
|
264
|
+
name = "Stdio-to-Streamable Http Proxy"
|
|
265
|
+
|
|
266
|
+
# Create a proxy server that connects to the client and exposes its capabilities
|
|
267
|
+
proxy = FastMCP.as_proxy(proxy_client, name=name)
|
|
268
|
+
if path is None:
|
|
269
|
+
path = "/mcp"
|
|
270
|
+
|
|
271
|
+
await proxy.run_async(transport="streamable-http", host=host, port=port, path=path)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@app.async_command("sse-proxy", help="Expose a stdio MCP server over SSE")
|
|
275
|
+
async def sse_proxy(
|
|
276
|
+
*,
|
|
277
|
+
command: Annotated[
|
|
278
|
+
str,
|
|
279
|
+
typer.Option(..., help="Command to start the MCP server (stdio transport)"),
|
|
280
|
+
],
|
|
281
|
+
host: Annotated[
|
|
282
|
+
Optional[str], typer.Option(help="Host to bind the proxy server on")
|
|
283
|
+
] = None,
|
|
284
|
+
port: Annotated[
|
|
285
|
+
Optional[int], typer.Option(help="Port to bind the proxy server on")
|
|
286
|
+
] = None,
|
|
287
|
+
path: Annotated[
|
|
288
|
+
Optional[str], typer.Option(help="SSE path to mount the proxy at")
|
|
289
|
+
] = None,
|
|
290
|
+
name: Annotated[
|
|
291
|
+
Optional[str], typer.Option(help="Display name for the proxy server")
|
|
292
|
+
] = None,
|
|
293
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
294
|
+
):
|
|
295
|
+
"""Expose a stdio-based MCP server over SSE."""
|
|
296
|
+
|
|
297
|
+
from fastmcp import FastMCP, Client
|
|
298
|
+
from fastmcp.client.transports import StdioTransport
|
|
299
|
+
|
|
300
|
+
parsed_command = shlex.split(command)
|
|
301
|
+
|
|
302
|
+
# Create a client that connects to the original server
|
|
303
|
+
proxy_client = Client(
|
|
304
|
+
transport=StdioTransport(
|
|
305
|
+
parsed_command[0], parsed_command[1:], _kv_to_dict(env)
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if name is None:
|
|
310
|
+
name = "Stdio-to-SSE Proxy"
|
|
311
|
+
|
|
312
|
+
# Create a proxy server that connects to the client and exposes its capabilities
|
|
313
|
+
proxy = FastMCP.as_proxy(proxy_client, name=name)
|
|
314
|
+
if path is None:
|
|
315
|
+
path = "/sse"
|
|
316
|
+
|
|
317
|
+
await proxy.run_async(transport="sse", host=host, port=port, path=path)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@app.async_command("stdio-service", help="Run a stdio MCP server as an HTTP service")
|
|
321
|
+
async def stdio_service(
|
|
322
|
+
*,
|
|
323
|
+
command: Annotated[
|
|
324
|
+
str,
|
|
325
|
+
typer.Option(
|
|
326
|
+
..., help="Command to start an MCP server over stdio (quoted string)"
|
|
327
|
+
),
|
|
328
|
+
],
|
|
329
|
+
host: Annotated[
|
|
330
|
+
Optional[str], typer.Option(help="Host to bind the service on")
|
|
331
|
+
] = None,
|
|
332
|
+
port: Annotated[
|
|
333
|
+
Optional[int], typer.Option(help="Port to bind the service on")
|
|
334
|
+
] = None,
|
|
335
|
+
webhook_secret: Annotated[
|
|
336
|
+
Optional[str],
|
|
337
|
+
typer.Option(help="Optional webhook secret for authenticating requests"),
|
|
338
|
+
] = None,
|
|
339
|
+
path: Annotated[
|
|
340
|
+
Optional[str], typer.Option(help="HTTP path to mount the service at")
|
|
341
|
+
] = None,
|
|
342
|
+
toolkit_name: Annotated[
|
|
343
|
+
Optional[str], typer.Option(help="Toolkit name to expose (default: mcp)")
|
|
344
|
+
] = None,
|
|
345
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
346
|
+
):
|
|
347
|
+
"""Run a stdio-based MCP server as an HTTP service."""
|
|
348
|
+
|
|
349
|
+
from mcp.client.session import ClientSession
|
|
350
|
+
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
351
|
+
|
|
352
|
+
from meshagent.mcp import MCPToolkit
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
parsed_command = shlex.split(command)
|
|
356
|
+
|
|
357
|
+
async with (
|
|
358
|
+
stdio_client(
|
|
359
|
+
StdioServerParameters(
|
|
360
|
+
command=parsed_command[0], # Executable
|
|
361
|
+
args=parsed_command[1:], # Optional command line arguments
|
|
362
|
+
env=_kv_to_dict(env), # Optional environment variables
|
|
363
|
+
)
|
|
364
|
+
) as (read_stream, write_stream)
|
|
365
|
+
):
|
|
366
|
+
async with ClientSession(
|
|
367
|
+
read_stream=read_stream, write_stream=write_stream
|
|
368
|
+
) as session:
|
|
369
|
+
mcp_tools_response = await session.list_tools()
|
|
370
|
+
|
|
371
|
+
if toolkit_name is None:
|
|
372
|
+
toolkit_name = "mcp"
|
|
373
|
+
|
|
374
|
+
toolkit = MCPToolkit(
|
|
375
|
+
name=toolkit_name, session=session, tools=mcp_tools_response.tools
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if port is None:
|
|
379
|
+
port = int(os.getenv("MESHAGENT_PORT", "8080"))
|
|
380
|
+
|
|
381
|
+
if host is None:
|
|
382
|
+
host = "0.0.0.0"
|
|
383
|
+
|
|
384
|
+
service_host = ServiceHost(
|
|
385
|
+
host=host, port=port, webhook_secret=webhook_secret
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if path is None:
|
|
389
|
+
path = "/service"
|
|
390
|
+
|
|
391
|
+
print(
|
|
392
|
+
f"[bold green]Starting service host on {host}:{port}{path}...[/bold green]"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
@service_host.path(path=path)
|
|
396
|
+
class CustomToolkit(RemoteToolkit):
|
|
397
|
+
def __init__(self):
|
|
398
|
+
super().__init__(
|
|
399
|
+
name=toolkit.name,
|
|
400
|
+
tools=toolkit.tools,
|
|
401
|
+
title=toolkit.title,
|
|
402
|
+
description=toolkit.description,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
await service_host.run()
|
|
406
|
+
|
|
407
|
+
except RoomException as e:
|
|
408
|
+
print(f"[red]{e}[/red]")
|