Skip to content
Closed
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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies = [

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

[project.scripts]
Expand All @@ -58,7 +58,7 @@ dev = [
"pytest-examples>=0.0.14",
"pytest-pretty>=1.2.0",
"inline-snapshot>=0.23.0",
"dirty-equals>=0.9.0",
"dirty-equals>=0.9.0"
]
docs = [
"mkdocs>=1.6.1",
Expand Down
62 changes: 57 additions & 5 deletions src/mcp/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys
from pathlib import Path
from typing import Annotated, Any
import tempfile

from mcp.server import FastMCP
from mcp.server import Server as LowLevelServer
Expand Down Expand Up @@ -149,6 +150,7 @@ def _check_server_object(server_object: Any, object_name: str):
Returns:
True if it's supported.
"""

if not isinstance(server_object, FastMCP):
logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting {FastMCP}).")
if isinstance(server_object, LowLevelServer):
Expand Down Expand Up @@ -304,10 +306,12 @@ def dev(

@app.command()
def run(
file_spec: str = typer.Argument(
...,
help="Python file to run, optionally with :object suffix",
),
file_spec: Annotated[
str | None,
typer.Argument(
help="Python file to run, optionally with :object suffix (omit when using --from)",
),
] = None,
transport: Annotated[
str | None,
typer.Option(
Expand All @@ -316,6 +320,13 @@ def run(
help="Transport protocol to use (stdio or sse)",
),
] = None,
from_url: Annotated[
str | None,
typer.Option(
"--from-github",
help="Run a remote MCP server directly from a Github URL (raw Github file or Github repo URL)",
),
] = None,
) -> None:
"""Run a MCP server.

Expand All @@ -327,6 +338,48 @@ def run(
all dependencies are available.\n
For dependency management, use `mcp install` or `mcp dev` instead.
""" # noqa: E501
if not file_spec and not from_url:
typer.echo("Error: You must provide either a file path or --from <url>")
raise typer.Exit(code=1)

if file_spec and from_url:
typer.echo("Error: You cannot specify both a file path and --from URL")
raise typer.Exit(code=1)

if not file_spec and from_url:
from urllib.parse import urlparse

ALLOWED_DOMAINS = {"github.com", "raw.githubusercontent.com"}

parsed = urlparse(from_url)
if parsed.netloc not in ALLOWED_DOMAINS:
logger.error(f"Invalid domain: {parsed.netloc}. Only GitHub URLs are supported.")
sys.exit(1)

if parsed.netloc == "github.com":
from_url = from_url.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/")
logger.info(f"Converted GitHub URL to raw: {from_url}")

logger.info(f"Fetching MCP server from {from_url}")

try:
import requests

with requests.get(from_url, stream=True, timeout=10) as res:
res.raise_for_status()
temp_dir = tempfile.mkdtemp(prefix="mcp_from_")
temp_path = Path(temp_dir) / "remote_server.py"

with open(temp_path, "wb") as f:
for chunk in res.iter_content(chunk_size=8192):
f.write(chunk)
except Exception as e:
logger.error(f"Failed to fetch server file: {e}")
sys.exit(1)

file_spec = str(temp_path)

assert file_spec is not None
file, server_object = _parse_file_path(file_spec)

logger.debug(
Expand All @@ -346,7 +399,6 @@ def run(
kwargs = {}
if transport:
kwargs["transport"] = transport

server.run(**kwargs)

except Exception:
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading