From ca4be52cd322d7721c6a43702ce7dfc7ef42be39 Mon Sep 17 00:00:00 2001 From: joshuazhou744 Date: Mon, 20 Oct 2025 15:28:32 -0400 Subject: [PATCH 1/3] feat: Remote Github file support for mcp run --- pyproject.toml | 2 +- src/mcp/cli/cli.py | 86 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4717cea92..7031577a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 4a7257a11..eea8284c3 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -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 @@ -149,11 +150,26 @@ 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}).") + try: + from fastmcp import FastMCP as ExternalFastMCP + except ImportError: + ExternalFastMCP = None + + valid_types = [FastMCP] + if ExternalFastMCP: + valid_types.append(ExternalFastMCP) + + if not isinstance(server_object, tuple(valid_types)): + logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting one of {valid_types}).") if isinstance(server_object, LowLevelServer): logger.warning( - "Note that only FastMCP server is supported. Low level Server class is not yet supported." + "Note that only FastMCP server (from ) is supported. Low level Server class is not yet supported." + ) + else: + logger.warning( + "Tip: Supported FastMCP classes come from either " + "`mcp.server.fastmcp.FastMCP` (SDK built-in) " + "or `fastmcp.FastMCP` (PyPI package)." ) return False return True @@ -304,10 +320,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( @@ -316,6 +334,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. @@ -327,8 +352,50 @@ def run( all dependencies are available.\n For dependency management, use `mcp install` or `mcp dev` instead. """ # noqa: E501 - file, server_object = _parse_file_path(file_spec) + if not file_spec and not from_url: + typer.echo("Error: You must provide either a file path or --from ") + 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( "Running server", extra={ @@ -346,7 +413,6 @@ def run( kwargs = {} if transport: kwargs["transport"] = transport - server.run(**kwargs) except Exception: @@ -485,4 +551,4 @@ def install( logger.info(f"Successfully installed {name} in Claude app") else: logger.error(f"Failed to install {name} in Claude app") - sys.exit(1) + sys.exit(1) \ No newline at end of file From baca2b6afc999d4b5efab71b29f1a4b704e815ff Mon Sep 17 00:00:00 2001 From: joshuazhou744 Date: Mon, 20 Oct 2025 16:14:12 -0400 Subject: [PATCH 2/3] remove support for other fastmcp module from jlowin/fastmcp --- pyproject.toml | 2 +- src/mcp/cli/cli.py | 32 ++++++++++---------------------- uv.lock | 2 ++ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7031577a7..e1ef574d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index eea8284c3..0313e07aa 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -150,27 +150,15 @@ def _check_server_object(server_object: Any, object_name: str): Returns: True if it's supported. """ - try: - from fastmcp import FastMCP as ExternalFastMCP - except ImportError: - ExternalFastMCP = None - - valid_types = [FastMCP] - if ExternalFastMCP: - valid_types.append(ExternalFastMCP) - if not isinstance(server_object, tuple(valid_types)): - logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting one of {valid_types}).") + 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): logger.warning( "Note that only FastMCP server (from ) is supported. Low level Server class is not yet supported." ) - else: - logger.warning( - "Tip: Supported FastMCP classes come from either " - "`mcp.server.fastmcp.FastMCP` (SDK built-in) " - "or `fastmcp.FastMCP` (PyPI package)." - ) return False return True @@ -324,7 +312,7 @@ def run( str | None, typer.Argument( help="Python file to run, optionally with :object suffix (omit when using --from)", - ) + ), ] = None, transport: Annotated[ str | None, @@ -339,7 +327,7 @@ def run( 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. @@ -360,7 +348,6 @@ def run( 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 @@ -374,11 +361,12 @@ def run( 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_") @@ -395,7 +383,7 @@ def run( assert file_spec is not None file, server_object = _parse_file_path(file_spec) - + logger.debug( "Running server", extra={ @@ -551,4 +539,4 @@ def install( logger.info(f"Successfully installed {name} in Claude app") else: logger.error(f"Failed to install {name} in Claude app") - sys.exit(1) \ No newline at end of file + sys.exit(1) diff --git a/uv.lock b/uv.lock index 6c6b13a6e..290822770 100644 --- a/uv.lock +++ b/uv.lock @@ -624,6 +624,7 @@ dependencies = [ [package.optional-dependencies] cli = [ { name = "python-dotenv" }, + { name = "requests" }, { name = "typer" }, ] rich = [ @@ -664,6 +665,7 @@ requires-dist = [ { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, { name = "python-multipart", specifier = ">=0.0.9" }, { name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=310" }, + { name = "requests", marker = "extra == 'cli'", specifier = ">=2.32.5" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, From 26bc4e9ea519ffe956f39f3c9dafe3a153db76bf Mon Sep 17 00:00:00 2001 From: joshuazhou744 Date: Mon, 20 Oct 2025 16:20:59 -0400 Subject: [PATCH 3/3] formatting issues --- src/mcp/cli/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/mcp/cli/cli.py b/src/mcp/cli/cli.py index 0313e07aa..ff82b98a3 100644 --- a/src/mcp/cli/cli.py +++ b/src/mcp/cli/cli.py @@ -152,12 +152,10 @@ def _check_server_object(server_object: Any, object_name: str): """ if not isinstance(server_object, FastMCP): - logger.error( - f"The server object {object_name} is of type {type(server_object)} (expecting {FastMCP})." - ) + logger.error(f"The server object {object_name} is of type {type(server_object)} (expecting {FastMCP}).") if isinstance(server_object, LowLevelServer): logger.warning( - "Note that only FastMCP server (from ) is supported. Low level Server class is not yet supported." + "Note that only FastMCP server is supported. Low level Server class is not yet supported." ) return False return True