mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 13:04:18 +02:00
Improve handling of editable installs
This commit is contained in:
parent
bffea91621
commit
0eaa84131a
7 changed files with 129 additions and 54 deletions
20
README.md
20
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
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from .server import FastMCP
|
||||
from .server import FastMCP
|
||||
from .tools import Image
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -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" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue