77import sys
88from pathlib import Path
99from typing import Annotated , Any
10+ import tempfile
1011
1112from mcp .server import FastMCP
1213from 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 ()
306322def 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