diff --git a/README.md b/README.md index 39f001980..7a5925e54 100644 --- a/README.md +++ b/README.md @@ -84,24 +84,34 @@ FastMCP includes a development server with the MCP Inspector for testing your se # Basic usage fastmcp dev your_server.py -# Load dependencies from current directory's pyproject.toml -fastmcp dev your_server.py --uv-directory . +# Install package in editable mode from current directory +fastmcp dev your_server.py --with-editable . # Install additional packages -fastmcp dev your_server.py --with pandas,numpy +fastmcp dev your_server.py --with pandas --with numpy # Combine both -fastmcp dev your_server.py --uv-directory . --with pandas,numpy +fastmcp dev your_server.py --with-editable . --with pandas --with numpy ``` -The `--with` flag automatically includes `fastmcp` and any additional packages you specify. The `--uv-directory` flag tells uv where to find your project's dependencies. +The `--with` flag automatically includes `fastmcp` and any additional packages you specify. The `--with-editable` flag installs the package from the specified directory in editable mode, which is useful during development. ### Installing in Claude To use your server with Claude Desktop: ```bash +# Basic usage fastmcp install your_server.py --name "My Server" + +# Install package in editable mode +fastmcp install your_server.py --with-editable . + +# Install additional packages +fastmcp install your_server.py --with pandas --with numpy + +# Combine options +fastmcp install your_server.py --with-editable . --with pandas --with numpy ``` diff --git a/src/fastmcp/__init__.py b/src/fastmcp/__init__.py index d6507801f..3946cf590 100644 --- a/src/fastmcp/__init__.py +++ b/src/fastmcp/__init__.py @@ -1 +1,2 @@ -from .server import FastMCP \ No newline at end of file +from .server import FastMCP +from .tools import Image diff --git a/src/fastmcp/cli.py b/src/fastmcp/app.py similarity index 100% rename from src/fastmcp/cli.py rename to src/fastmcp/app.py diff --git a/src/fastmcp/cli/claude.py b/src/fastmcp/cli/claude.py index 850b08e8e..998733b60 100644 --- a/src/fastmcp/cli/claude.py +++ b/src/fastmcp/cli/claude.py @@ -28,7 +28,9 @@ def update_claude_config( file: Path, server_name: Optional[str] = None, *, - uv_directory: Optional[Path] = None, + with_editable: Optional[Path] = None, + with_packages: Optional[list[str]] = None, + force: bool = False, ) -> bool: """Add the MCP server to Claude's configuration. @@ -36,7 +38,9 @@ def update_claude_config( file: Path to the server file server_name: Optional custom name for the server. If not provided, defaults to the file stem - uv_directory: Optional directory containing pyproject.toml + 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 """ config_dir = get_claude_config_path() if not config_dir: @@ -54,17 +58,34 @@ def update_claude_config( # Use provided server_name or fall back to file stem name = server_name or file.stem if name in config["mcpServers"]: - logger.warning( - f"Server '{name}' already exists in Claude config", + if not force: + logger.warning( + f"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", extra={"config_file": str(config_file)}, ) - return False # Build uv run command - args = [] - if uv_directory: - args.extend(["--directory", str(uv_directory)]) - args.extend(["run", str(file)]) + args = ["run"] + + 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)) config["mcpServers"][name] = { "command": "uv", diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index f65df3c9f..64772095a 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -25,17 +25,17 @@ app = typer.Typer( def _build_uv_command( file: Path, - uv_directory: Optional[Path] = None, + with_editable: Optional[Path] = None, with_packages: Optional[list[str]] = None, ) -> list[str]: """Build the uv run command.""" cmd = ["uv"] - if uv_directory: - cmd.extend(["--directory", str(uv_directory)]) - cmd.extend(["run", "--with", "fastmcp"]) + if with_editable: + cmd.extend(["--with-editable", str(with_editable)]) + if with_packages: for pkg in with_packages: if pkg: @@ -145,12 +145,12 @@ def dev( ..., help="Python file to run, optionally with :object suffix", ), - uv_directory: Annotated[ + with_editable: Annotated[ Optional[Path], typer.Option( - "--uv-directory", - "-d", - help="Directory containing pyproject.toml (defaults to current directory)", + "--with-editable", + "-e", + help="Directory containing pyproject.toml to install in editable mode", exists=True, file_okay=False, resolve_path=True, @@ -172,13 +172,13 @@ def dev( extra={ "file": str(file), "server_object": server_object, - "uv_directory": str(uv_directory) if uv_directory else None, + "with_editable": str(with_editable) if with_editable else None, "with_packages": with_packages, }, ) try: - uv_cmd = _build_uv_command(file, uv_directory, with_packages) + uv_cmd = _build_uv_command(file, with_editable, with_packages) # Run the MCP Inspector command process = subprocess.run( ["npx", "@modelcontextprotocol/inspector"] + uv_cmd, @@ -217,12 +217,12 @@ def run( help="Transport protocol to use (stdio or sse)", ), ] = None, - uv_directory: Annotated[ + with_editable: Annotated[ Optional[Path], typer.Option( - "--uv-directory", - "-d", - help="Directory containing pyproject.toml (defaults to current directory)", + "--with-editable", + "-e", + help="Directory containing pyproject.toml to install in editable mode", exists=True, file_okay=False, resolve_path=True, @@ -238,7 +238,7 @@ def run( "file": str(file), "server_object": server_object, "transport": transport, - "uv_directory": str(uv_directory) if uv_directory else None, + "with_editable": str(with_editable) if with_editable else None, }, ) @@ -278,17 +278,32 @@ def install( help="Custom name for the server (defaults to file name)", ), ] = None, - uv_directory: Annotated[ + with_editable: Annotated[ Optional[Path], typer.Option( - "--uv-directory", - "-d", - help="Directory containing pyproject.toml (defaults to current directory)", + "--with-editable", + "-e", + help="Directory containing pyproject.toml to install in editable mode", exists=True, file_okay=False, resolve_path=True, ), ] = None, + with_packages: Annotated[ + list[str], + typer.Option( + "--with", + help="Additional packages to install", + ), + ] = [], + force: Annotated[ + bool, + typer.Option( + "--force", + "-f", + help="Replace existing server if one exists with the same name", + ), + ] = False, ) -> None: """Install a FastMCP server in the Claude desktop app.""" file, server_object = _parse_file_path(file_spec) @@ -299,7 +314,9 @@ def install( "file": str(file), "server_name": server_name, "server_object": server_object, - "uv_directory": str(uv_directory) if uv_directory else None, + "with_editable": str(with_editable) if with_editable else None, + "with_packages": with_packages, + "force": force, }, ) @@ -307,7 +324,13 @@ def install( logger.error("Claude app not found") sys.exit(1) - if claude.update_claude_config(file, server_name, uv_directory=uv_directory): + if claude.update_claude_config( + file, + server_name, + with_editable=with_editable, + with_packages=with_packages, + force=force, + ): name = server_name or file.stem print(f"Successfully installed {name} in Claude app") else: diff --git a/src/fastmcp/tools.py b/src/fastmcp/tools.py index 42515dd62..67e474ff7 100644 --- a/src/fastmcp/tools.py +++ b/src/fastmcp/tools.py @@ -17,26 +17,46 @@ logger = get_logger(__name__) class Image: """Helper class for returning images from tools.""" - def __init__(self, path: Union[str, Path], mime_type: Optional[str] = None): - self.path = Path(path) - self.mime_type = mime_type or self._guess_mime_type() + def __init__( + self, + path: Optional[Union[str, Path]] = None, + data: Optional[bytes] = None, + format: Optional[str] = None, + ): + if path is None and data is None: + raise ValueError("Either path or data must be provided") + if path is not None and data is not None: + raise ValueError("Only one of path or data can be provided") - def _guess_mime_type(self) -> str: - """Guess MIME type from file extension.""" - suffix = self.path.suffix.lower() - return { - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - }.get(suffix, "application/octet-stream") + self.path = Path(path) if path else None + self.data = data + self._format = format + self._mime_type = self._get_mime_type() + + def _get_mime_type(self) -> str: + """Get MIME type from format or guess from file extension.""" + if self._format: + return f"image/{self._format.lower()}" + + if self.path: + suffix = self.path.suffix.lower() + return { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + }.get(suffix, "application/octet-stream") + return "image/png" # default for raw binary data def to_image_content(self) -> ImageContent: """Convert to MCP ImageContent.""" - with open(self.path, "rb") as f: - data = base64.b64encode(f.read()).decode() - return ImageContent(type="image", data=data, mimeType=self.mime_type) + if self.path: + with open(self.path, "rb") as f: + data = base64.b64encode(f.read()).decode() + else: + data = base64.b64encode(self.data).decode() + return ImageContent(type="image", data=data, mimeType=self._mime_type) class Tool(BaseModel): diff --git a/uv.lock b/uv.lock index 0c906e95e..aefa7b395 100644 --- a/uv.lock +++ b/uv.lock @@ -222,7 +222,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "0.1.1.dev1+gca7438f.d20241130" +version = "0.1.1.dev3+gbffea91.d20241130" source = { editable = "." } dependencies = [ { name = "httpx" },