Skip to content

Commit ca4be52

Browse files
committed
feat: Remote Github file support for mcp run
1 parent 40acbc5 commit ca4be52

File tree

2 files changed

+77
-11
lines changed

2 files changed

+77
-11
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies = [
3737

3838
[project.optional-dependencies]
3939
rich = ["rich>=13.9.4"]
40-
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
40+
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0", "requests>=2.32.5"]
4141
ws = ["websockets>=15.0.1"]
4242

4343
[project.scripts]

src/mcp/cli/cli.py

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
from pathlib import Path
99
from typing import Annotated, Any
10+
import tempfile
1011

1112
from mcp.server import FastMCP
1213
from mcp.server import Server as LowLevelServer
@@ -149,11 +150,26 @@ def _check_server_object(server_object: Any, object_name: str):
149150
Returns:
150151
True if it's supported.
151152
"""
152-
if not isinstance(server_object, FastMCP):
153-
logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting {FastMCP}).")
153+
try:
154+
from fastmcp import FastMCP as ExternalFastMCP
155+
except ImportError:
156+
ExternalFastMCP = None
157+
158+
valid_types = [FastMCP]
159+
if ExternalFastMCP:
160+
valid_types.append(ExternalFastMCP)
161+
162+
if not isinstance(server_object, tuple(valid_types)):
163+
logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting one of {valid_types}).")
154164
if isinstance(server_object, LowLevelServer):
155165
logger.warning(
156-
"Note that only FastMCP server is supported. Low level Server class is not yet supported."
166+
"Note that only FastMCP server (from ) is supported. Low level Server class is not yet supported."
167+
)
168+
else:
169+
logger.warning(
170+
"Tip: Supported FastMCP classes come from either "
171+
"`mcp.server.fastmcp.FastMCP` (SDK built-in) "
172+
"or `fastmcp.FastMCP` (PyPI package)."
157173
)
158174
return False
159175
return True
@@ -304,10 +320,12 @@ def dev(
304320

305321
@app.command()
306322
def run(
307-
file_spec: str = typer.Argument(
308-
...,
309-
help="Python file to run, optionally with :object suffix",
310-
),
323+
file_spec: Annotated[
324+
str | None,
325+
typer.Argument(
326+
help="Python file to run, optionally with :object suffix (omit when using --from)",
327+
)
328+
] = None,
311329
transport: Annotated[
312330
str | None,
313331
typer.Option(
@@ -316,6 +334,13 @@ def run(
316334
help="Transport protocol to use (stdio or sse)",
317335
),
318336
] = None,
337+
from_url: Annotated[
338+
str | None,
339+
typer.Option(
340+
"--from-github",
341+
help="Run a remote MCP server directly from a Github URL (raw Github file or Github repo URL)",
342+
)
343+
] = None,
319344
) -> None:
320345
"""Run a MCP server.
321346
@@ -327,8 +352,50 @@ def run(
327352
all dependencies are available.\n
328353
For dependency management, use `mcp install` or `mcp dev` instead.
329354
""" # noqa: E501
330-
file, server_object = _parse_file_path(file_spec)
355+
if not file_spec and not from_url:
356+
typer.echo("Error: You must provide either a file path or --from <url>")
357+
raise typer.Exit(code=1)
358+
359+
if file_spec and from_url:
360+
typer.echo("Error: You cannot specify both a file path and --from URL")
361+
raise typer.Exit(code=1)
362+
363+
364+
if not file_spec and from_url:
365+
from urllib.parse import urlparse
366+
367+
ALLOWED_DOMAINS = {"github.com", "raw.githubusercontent.com"}
368+
369+
parsed = urlparse(from_url)
370+
if parsed.netloc not in ALLOWED_DOMAINS:
371+
logger.error(f"Invalid domain: {parsed.netloc}. Only GitHub URLs are supported.")
372+
sys.exit(1)
331373

374+
if parsed.netloc == "github.com":
375+
from_url = from_url.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/")
376+
logger.info(f"Converted GitHub URL to raw: {from_url}")
377+
378+
logger.info(f"Fetching MCP server from {from_url}")
379+
380+
try:
381+
import requests
382+
with requests.get(from_url, stream=True, timeout=10) as res:
383+
res.raise_for_status()
384+
temp_dir = tempfile.mkdtemp(prefix="mcp_from_")
385+
temp_path = Path(temp_dir) / "remote_server.py"
386+
387+
with open(temp_path, "wb") as f:
388+
for chunk in res.iter_content(chunk_size=8192):
389+
f.write(chunk)
390+
except Exception as e:
391+
logger.error(f"Failed to fetch server file: {e}")
392+
sys.exit(1)
393+
394+
file_spec = str(temp_path)
395+
396+
assert file_spec is not None
397+
file, server_object = _parse_file_path(file_spec)
398+
332399
logger.debug(
333400
"Running server",
334401
extra={
@@ -346,7 +413,6 @@ def run(
346413
kwargs = {}
347414
if transport:
348415
kwargs["transport"] = transport
349-
350416
server.run(**kwargs)
351417

352418
except Exception:
@@ -485,4 +551,4 @@ def install(
485551
logger.info(f"Successfully installed {name} in Claude app")
486552
else:
487553
logger.error(f"Failed to install {name} in Claude app")
488-
sys.exit(1)
554+
sys.exit(1)

0 commit comments

Comments
 (0)