diff --git a/examples/desktop.py b/examples/desktop.py index 7c708ce..a9b46cc 100644 --- a/examples/desktop.py +++ b/examples/desktop.py @@ -23,7 +23,3 @@ def desktop() -> list[str]: def add(a: int, b: int) -> int: """Add two numbers""" return a + b - - -if __name__ == "__main__": - mcp.run() diff --git a/examples/echo.py b/examples/echo.py index 86349ee..2ac9edc 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -15,8 +15,13 @@ def echo_tool(text: str) -> str: return text +@mcp.resource("echo://static") +def echo_resource() -> str: + return "Echo!" + + @mcp.resource("echo://{text}") -def echo_resource(text: str) -> str: +def echo_template(text: str) -> str: """Echo the input text""" return f"Echo: {text}" @@ -24,7 +29,3 @@ def echo_resource(text: str) -> str: @mcp.prompt("echo") def echo_prompt(text: str) -> str: return text - - -if __name__ == "__main__": - mcp.run() diff --git a/examples/screenshot.py b/examples/screenshot.py index bfef5d8..012a253 100644 --- a/examples/screenshot.py +++ b/examples/screenshot.py @@ -26,7 +26,3 @@ def take_screenshot() -> Image: # if the file exceeds ~1MB, it will be rejected by Claude screenshot.convert("RGB").save(buffer, format="JPEG", quality=60, optimize=True) return Image(data=buffer.getvalue(), format="jpeg") - - -if __name__ == "__main__": - mcp.run() diff --git a/examples/simple_echo.py b/examples/simple_echo.py index 2a81ca4..99f52c6 100644 --- a/examples/simple_echo.py +++ b/examples/simple_echo.py @@ -13,7 +13,3 @@ def echo(text: str) -> str: """Echo the input text""" return text - - -if __name__ == "__main__": - mcp.run() diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py index 998733b..be216a7 100644 --- a/src/fastmcp/cli/claude.py +++ b/src/fastmcp/cli/claude.py @@ -25,8 +25,8 @@ def get_claude_config_path() -> Path | None: def update_claude_config( - file: Path, - server_name: Optional[str] = None, + file_spec: str, + server_name: str, *, with_editable: Optional[Path] = None, with_packages: Optional[list[str]] = None, @@ -35,9 +35,8 @@ def update_claude_config( """Add the MCP server to Claude's configuration. Args: - file: Path to the server file - server_name: Optional custom name for the server. If not provided, - defaults to the file stem + file_spec: Path to the server file, optionally with :object suffix + server_name: Name for the server in Claude's config with_editable: Optional directory to install in editable mode with_packages: Optional list of additional packages to install force: If True, replace existing server with same name @@ -55,46 +54,49 @@ def update_claude_config( if "mcpServers" not in config: config["mcpServers"] = {} - # Use provided server_name or fall back to file stem - name = server_name or file.stem - if name in config["mcpServers"]: + if server_name in config["mcpServers"]: if not force: logger.warning( - f"Server '{name}' already exists in Claude config. " + f"Server '{server_name}' already exists in Claude config. " "Use `--force` to replace.", extra={"config_file": str(config_file)}, ) return False logger.info( - f"Replacing existing server '{name}' in Claude config", + f"Replacing existing server '{server_name}' in Claude config", extra={"config_file": str(config_file)}, ) # Build uv run command - args = ["run"] + args = ["run", "--with", "fastmcp"] if with_editable: args.extend(["--with-editable", str(with_editable)]) - # Always include fastmcp - args.extend(["--with", "fastmcp"]) - - # Add additional packages if with_packages: for pkg in with_packages: if pkg: args.extend(["--with", pkg]) - args.append(str(file)) + # Convert file path to absolute before adding to command + # Split off any :object suffix first + if ":" in file_spec: + file_path, server_object = file_spec.rsplit(":", 1) + file_spec = f"{Path(file_path).resolve()}:{server_object}" + else: + file_spec = str(Path(file_spec).resolve()) + + # Add fastmcp run command + args.extend(["fastmcp", "run", file_spec]) - config["mcpServers"][name] = { + config["mcpServers"][server_name] = { "command": "uv", "args": args, } config_file.write_text(json.dumps(config, indent=2)) logger.info( - f"Added server '{name}' to Claude config", + f"Added server '{server_name}' to Claude config", extra={"config_file": str(config_file)}, ) return True diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 42e6325..cab03aa 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -24,11 +24,11 @@ def _build_uv_command( - file: Path, + file_spec: str, with_editable: Optional[Path] = None, with_packages: Optional[list[str]] = None, ) -> list[str]: - """Build the uv run command.""" + """Build the uv run command that runs a FastMCP server through fastmcp run.""" cmd = ["uv"] cmd.extend(["run", "--with", "fastmcp"]) @@ -41,7 +41,8 @@ def _build_uv_command( if pkg: cmd.extend(["--with", pkg]) - cmd.append(str(file)) + # Add fastmcp run command + cmd.extend(["fastmcp", "run", file_spec]) return cmd @@ -89,7 +90,7 @@ def _import_server(file: Path, server_object: Optional[str] = None): module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) - # If no object specified, try __main__ block + # If no object specified, try common server names if not server_object: # Look for the most common server object names for name in ["mcp", "server", "app"]: @@ -97,7 +98,9 @@ def _import_server(file: Path, server_object: Optional[str] = None): return getattr(module, name) logger.error( - f"No server object found in {file}. Please specify the object name with file:object syntax.", + f"No server object found in {file}. Please either:\n" + "1. Use a standard variable name (mcp, server, or app)\n" + "2. Specify the object name with file:object syntax", extra={"file": str(file)}, ) sys.exit(1) @@ -178,7 +181,7 @@ def dev( ) try: - uv_cmd = _build_uv_command(file, with_editable, with_packages) + uv_cmd = _build_uv_command(file_spec, with_editable, with_packages) # Run the MCP Inspector command process = subprocess.run( ["npx", "@modelcontextprotocol/inspector"] + uv_cmd, @@ -229,7 +232,12 @@ def run( ), ] = None, ) -> None: - """Run a FastMCP server.""" + """Run a FastMCP server. + + The server can be specified in two ways: + 1. Module approach: server.py - runs the module directly, expecting a server.run() call + 2. Import approach: server.py:app - imports and runs the specified server object + """ file, server_object = _parse_file_path(file_spec) logger.debug( @@ -338,7 +346,7 @@ def install( name = file.stem if claude.update_claude_config( - file, + file_spec, name, with_editable=with_editable, with_packages=with_packages, @@ -348,7 +356,3 @@ def install( else: print(f"Failed to install {name} in Claude app") sys.exit(1) - - -if __name__ == "__main__": - app()