Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
0d751b2
1
Parent(s): a0bed44
Improve handling of editable installs
Browse files- README.md +15 -5
- src/fastmcp/__init__.py +2 -1
- src/fastmcp/{cli.py → app.py} +0 -0
- src/fastmcp/cli/claude.py +30 -9
- src/fastmcp/cli/cli.py +44 -21
- src/fastmcp/tools.py +37 -17
- uv.lock +1 -1
README.md
CHANGED
|
@@ -84,24 +84,34 @@ FastMCP includes a development server with the MCP Inspector for testing your se
|
|
| 84 |
# Basic usage
|
| 85 |
fastmcp dev your_server.py
|
| 86 |
|
| 87 |
-
#
|
| 88 |
-
fastmcp dev your_server.py --
|
| 89 |
|
| 90 |
# Install additional packages
|
| 91 |
-
fastmcp dev your_server.py --with pandas
|
| 92 |
|
| 93 |
# Combine both
|
| 94 |
-
fastmcp dev your_server.py --
|
| 95 |
```
|
| 96 |
|
| 97 |
-
The `--with` flag automatically includes `fastmcp` and any additional packages you specify. The `--
|
| 98 |
|
| 99 |
### Installing in Claude
|
| 100 |
|
| 101 |
To use your server with Claude Desktop:
|
| 102 |
|
| 103 |
```bash
|
|
|
|
| 104 |
fastmcp install your_server.py --name "My Server"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
```
|
| 106 |
|
| 107 |
|
|
|
|
| 84 |
# Basic usage
|
| 85 |
fastmcp dev your_server.py
|
| 86 |
|
| 87 |
+
# Install package in editable mode from current directory
|
| 88 |
+
fastmcp dev your_server.py --with-editable .
|
| 89 |
|
| 90 |
# Install additional packages
|
| 91 |
+
fastmcp dev your_server.py --with pandas --with numpy
|
| 92 |
|
| 93 |
# Combine both
|
| 94 |
+
fastmcp dev your_server.py --with-editable . --with pandas --with numpy
|
| 95 |
```
|
| 96 |
|
| 97 |
+
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.
|
| 98 |
|
| 99 |
### Installing in Claude
|
| 100 |
|
| 101 |
To use your server with Claude Desktop:
|
| 102 |
|
| 103 |
```bash
|
| 104 |
+
# Basic usage
|
| 105 |
fastmcp install your_server.py --name "My Server"
|
| 106 |
+
|
| 107 |
+
# Install package in editable mode
|
| 108 |
+
fastmcp install your_server.py --with-editable .
|
| 109 |
+
|
| 110 |
+
# Install additional packages
|
| 111 |
+
fastmcp install your_server.py --with pandas --with numpy
|
| 112 |
+
|
| 113 |
+
# Combine options
|
| 114 |
+
fastmcp install your_server.py --with-editable . --with pandas --with numpy
|
| 115 |
```
|
| 116 |
|
| 117 |
|
src/fastmcp/__init__.py
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
from .server import FastMCP
|
|
|
|
|
|
| 1 |
+
from .server import FastMCP
|
| 2 |
+
from .tools import Image
|
src/fastmcp/{cli.py → app.py}
RENAMED
|
File without changes
|
src/fastmcp/cli/claude.py
CHANGED
|
@@ -28,7 +28,9 @@ def update_claude_config(
|
|
| 28 |
file: Path,
|
| 29 |
server_name: Optional[str] = None,
|
| 30 |
*,
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
) -> bool:
|
| 33 |
"""Add the MCP server to Claude's configuration.
|
| 34 |
|
|
@@ -36,7 +38,9 @@ def update_claude_config(
|
|
| 36 |
file: Path to the server file
|
| 37 |
server_name: Optional custom name for the server. If not provided,
|
| 38 |
defaults to the file stem
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
"""
|
| 41 |
config_dir = get_claude_config_path()
|
| 42 |
if not config_dir:
|
|
@@ -54,17 +58,34 @@ def update_claude_config(
|
|
| 54 |
# Use provided server_name or fall back to file stem
|
| 55 |
name = server_name or file.stem
|
| 56 |
if name in config["mcpServers"]:
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
extra={"config_file": str(config_file)},
|
| 60 |
)
|
| 61 |
-
return False
|
| 62 |
|
| 63 |
# Build uv run command
|
| 64 |
-
args = []
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
config["mcpServers"][name] = {
|
| 70 |
"command": "uv",
|
|
|
|
| 28 |
file: Path,
|
| 29 |
server_name: Optional[str] = None,
|
| 30 |
*,
|
| 31 |
+
with_editable: Optional[Path] = None,
|
| 32 |
+
with_packages: Optional[list[str]] = None,
|
| 33 |
+
force: bool = False,
|
| 34 |
) -> bool:
|
| 35 |
"""Add the MCP server to Claude's configuration.
|
| 36 |
|
|
|
|
| 38 |
file: Path to the server file
|
| 39 |
server_name: Optional custom name for the server. If not provided,
|
| 40 |
defaults to the file stem
|
| 41 |
+
with_editable: Optional directory to install in editable mode
|
| 42 |
+
with_packages: Optional list of additional packages to install
|
| 43 |
+
force: If True, replace existing server with same name
|
| 44 |
"""
|
| 45 |
config_dir = get_claude_config_path()
|
| 46 |
if not config_dir:
|
|
|
|
| 58 |
# Use provided server_name or fall back to file stem
|
| 59 |
name = server_name or file.stem
|
| 60 |
if name in config["mcpServers"]:
|
| 61 |
+
if not force:
|
| 62 |
+
logger.warning(
|
| 63 |
+
f"Server '{name}' already exists in Claude config. "
|
| 64 |
+
"Use `--force` to replace.",
|
| 65 |
+
extra={"config_file": str(config_file)},
|
| 66 |
+
)
|
| 67 |
+
return False
|
| 68 |
+
logger.info(
|
| 69 |
+
f"Replacing existing server '{name}' in Claude config",
|
| 70 |
extra={"config_file": str(config_file)},
|
| 71 |
)
|
|
|
|
| 72 |
|
| 73 |
# Build uv run command
|
| 74 |
+
args = ["run"]
|
| 75 |
+
|
| 76 |
+
if with_editable:
|
| 77 |
+
args.extend(["--with-editable", str(with_editable)])
|
| 78 |
+
|
| 79 |
+
# Always include fastmcp
|
| 80 |
+
args.extend(["--with", "fastmcp"])
|
| 81 |
+
|
| 82 |
+
# Add additional packages
|
| 83 |
+
if with_packages:
|
| 84 |
+
for pkg in with_packages:
|
| 85 |
+
if pkg:
|
| 86 |
+
args.extend(["--with", pkg])
|
| 87 |
+
|
| 88 |
+
args.append(str(file))
|
| 89 |
|
| 90 |
config["mcpServers"][name] = {
|
| 91 |
"command": "uv",
|
src/fastmcp/cli/cli.py
CHANGED
|
@@ -25,17 +25,17 @@ app = typer.Typer(
|
|
| 25 |
|
| 26 |
def _build_uv_command(
|
| 27 |
file: Path,
|
| 28 |
-
|
| 29 |
with_packages: Optional[list[str]] = None,
|
| 30 |
) -> list[str]:
|
| 31 |
"""Build the uv run command."""
|
| 32 |
cmd = ["uv"]
|
| 33 |
|
| 34 |
-
if uv_directory:
|
| 35 |
-
cmd.extend(["--directory", str(uv_directory)])
|
| 36 |
-
|
| 37 |
cmd.extend(["run", "--with", "fastmcp"])
|
| 38 |
|
|
|
|
|
|
|
|
|
|
| 39 |
if with_packages:
|
| 40 |
for pkg in with_packages:
|
| 41 |
if pkg:
|
|
@@ -145,12 +145,12 @@ def dev(
|
|
| 145 |
...,
|
| 146 |
help="Python file to run, optionally with :object suffix",
|
| 147 |
),
|
| 148 |
-
|
| 149 |
Optional[Path],
|
| 150 |
typer.Option(
|
| 151 |
-
"--
|
| 152 |
-
"-
|
| 153 |
-
help="Directory containing pyproject.toml
|
| 154 |
exists=True,
|
| 155 |
file_okay=False,
|
| 156 |
resolve_path=True,
|
|
@@ -172,13 +172,13 @@ def dev(
|
|
| 172 |
extra={
|
| 173 |
"file": str(file),
|
| 174 |
"server_object": server_object,
|
| 175 |
-
"
|
| 176 |
"with_packages": with_packages,
|
| 177 |
},
|
| 178 |
)
|
| 179 |
|
| 180 |
try:
|
| 181 |
-
uv_cmd = _build_uv_command(file,
|
| 182 |
# Run the MCP Inspector command
|
| 183 |
process = subprocess.run(
|
| 184 |
["npx", "@modelcontextprotocol/inspector"] + uv_cmd,
|
|
@@ -217,12 +217,12 @@ def run(
|
|
| 217 |
help="Transport protocol to use (stdio or sse)",
|
| 218 |
),
|
| 219 |
] = None,
|
| 220 |
-
|
| 221 |
Optional[Path],
|
| 222 |
typer.Option(
|
| 223 |
-
"--
|
| 224 |
-
"-
|
| 225 |
-
help="Directory containing pyproject.toml
|
| 226 |
exists=True,
|
| 227 |
file_okay=False,
|
| 228 |
resolve_path=True,
|
|
@@ -238,7 +238,7 @@ def run(
|
|
| 238 |
"file": str(file),
|
| 239 |
"server_object": server_object,
|
| 240 |
"transport": transport,
|
| 241 |
-
"
|
| 242 |
},
|
| 243 |
)
|
| 244 |
|
|
@@ -278,17 +278,32 @@ def install(
|
|
| 278 |
help="Custom name for the server (defaults to file name)",
|
| 279 |
),
|
| 280 |
] = None,
|
| 281 |
-
|
| 282 |
Optional[Path],
|
| 283 |
typer.Option(
|
| 284 |
-
"--
|
| 285 |
-
"-
|
| 286 |
-
help="Directory containing pyproject.toml
|
| 287 |
exists=True,
|
| 288 |
file_okay=False,
|
| 289 |
resolve_path=True,
|
| 290 |
),
|
| 291 |
] = None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
) -> None:
|
| 293 |
"""Install a FastMCP server in the Claude desktop app."""
|
| 294 |
file, server_object = _parse_file_path(file_spec)
|
|
@@ -299,7 +314,9 @@ def install(
|
|
| 299 |
"file": str(file),
|
| 300 |
"server_name": server_name,
|
| 301 |
"server_object": server_object,
|
| 302 |
-
"
|
|
|
|
|
|
|
| 303 |
},
|
| 304 |
)
|
| 305 |
|
|
@@ -307,7 +324,13 @@ def install(
|
|
| 307 |
logger.error("Claude app not found")
|
| 308 |
sys.exit(1)
|
| 309 |
|
| 310 |
-
if claude.update_claude_config(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
name = server_name or file.stem
|
| 312 |
print(f"Successfully installed {name} in Claude app")
|
| 313 |
else:
|
|
|
|
| 25 |
|
| 26 |
def _build_uv_command(
|
| 27 |
file: Path,
|
| 28 |
+
with_editable: Optional[Path] = None,
|
| 29 |
with_packages: Optional[list[str]] = None,
|
| 30 |
) -> list[str]:
|
| 31 |
"""Build the uv run command."""
|
| 32 |
cmd = ["uv"]
|
| 33 |
|
|
|
|
|
|
|
|
|
|
| 34 |
cmd.extend(["run", "--with", "fastmcp"])
|
| 35 |
|
| 36 |
+
if with_editable:
|
| 37 |
+
cmd.extend(["--with-editable", str(with_editable)])
|
| 38 |
+
|
| 39 |
if with_packages:
|
| 40 |
for pkg in with_packages:
|
| 41 |
if pkg:
|
|
|
|
| 145 |
...,
|
| 146 |
help="Python file to run, optionally with :object suffix",
|
| 147 |
),
|
| 148 |
+
with_editable: Annotated[
|
| 149 |
Optional[Path],
|
| 150 |
typer.Option(
|
| 151 |
+
"--with-editable",
|
| 152 |
+
"-e",
|
| 153 |
+
help="Directory containing pyproject.toml to install in editable mode",
|
| 154 |
exists=True,
|
| 155 |
file_okay=False,
|
| 156 |
resolve_path=True,
|
|
|
|
| 172 |
extra={
|
| 173 |
"file": str(file),
|
| 174 |
"server_object": server_object,
|
| 175 |
+
"with_editable": str(with_editable) if with_editable else None,
|
| 176 |
"with_packages": with_packages,
|
| 177 |
},
|
| 178 |
)
|
| 179 |
|
| 180 |
try:
|
| 181 |
+
uv_cmd = _build_uv_command(file, with_editable, with_packages)
|
| 182 |
# Run the MCP Inspector command
|
| 183 |
process = subprocess.run(
|
| 184 |
["npx", "@modelcontextprotocol/inspector"] + uv_cmd,
|
|
|
|
| 217 |
help="Transport protocol to use (stdio or sse)",
|
| 218 |
),
|
| 219 |
] = None,
|
| 220 |
+
with_editable: Annotated[
|
| 221 |
Optional[Path],
|
| 222 |
typer.Option(
|
| 223 |
+
"--with-editable",
|
| 224 |
+
"-e",
|
| 225 |
+
help="Directory containing pyproject.toml to install in editable mode",
|
| 226 |
exists=True,
|
| 227 |
file_okay=False,
|
| 228 |
resolve_path=True,
|
|
|
|
| 238 |
"file": str(file),
|
| 239 |
"server_object": server_object,
|
| 240 |
"transport": transport,
|
| 241 |
+
"with_editable": str(with_editable) if with_editable else None,
|
| 242 |
},
|
| 243 |
)
|
| 244 |
|
|
|
|
| 278 |
help="Custom name for the server (defaults to file name)",
|
| 279 |
),
|
| 280 |
] = None,
|
| 281 |
+
with_editable: Annotated[
|
| 282 |
Optional[Path],
|
| 283 |
typer.Option(
|
| 284 |
+
"--with-editable",
|
| 285 |
+
"-e",
|
| 286 |
+
help="Directory containing pyproject.toml to install in editable mode",
|
| 287 |
exists=True,
|
| 288 |
file_okay=False,
|
| 289 |
resolve_path=True,
|
| 290 |
),
|
| 291 |
] = None,
|
| 292 |
+
with_packages: Annotated[
|
| 293 |
+
list[str],
|
| 294 |
+
typer.Option(
|
| 295 |
+
"--with",
|
| 296 |
+
help="Additional packages to install",
|
| 297 |
+
),
|
| 298 |
+
] = [],
|
| 299 |
+
force: Annotated[
|
| 300 |
+
bool,
|
| 301 |
+
typer.Option(
|
| 302 |
+
"--force",
|
| 303 |
+
"-f",
|
| 304 |
+
help="Replace existing server if one exists with the same name",
|
| 305 |
+
),
|
| 306 |
+
] = False,
|
| 307 |
) -> None:
|
| 308 |
"""Install a FastMCP server in the Claude desktop app."""
|
| 309 |
file, server_object = _parse_file_path(file_spec)
|
|
|
|
| 314 |
"file": str(file),
|
| 315 |
"server_name": server_name,
|
| 316 |
"server_object": server_object,
|
| 317 |
+
"with_editable": str(with_editable) if with_editable else None,
|
| 318 |
+
"with_packages": with_packages,
|
| 319 |
+
"force": force,
|
| 320 |
},
|
| 321 |
)
|
| 322 |
|
|
|
|
| 324 |
logger.error("Claude app not found")
|
| 325 |
sys.exit(1)
|
| 326 |
|
| 327 |
+
if claude.update_claude_config(
|
| 328 |
+
file,
|
| 329 |
+
server_name,
|
| 330 |
+
with_editable=with_editable,
|
| 331 |
+
with_packages=with_packages,
|
| 332 |
+
force=force,
|
| 333 |
+
):
|
| 334 |
name = server_name or file.stem
|
| 335 |
print(f"Successfully installed {name} in Claude app")
|
| 336 |
else:
|
src/fastmcp/tools.py
CHANGED
|
@@ -17,26 +17,46 @@ logger = get_logger(__name__)
|
|
| 17 |
class Image:
|
| 18 |
"""Helper class for returning images from tools."""
|
| 19 |
|
| 20 |
-
def __init__(
|
| 21 |
-
self
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
"
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
def to_image_content(self) -> ImageContent:
|
| 36 |
"""Convert to MCP ImageContent."""
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
class Tool(BaseModel):
|
|
|
|
| 17 |
class Image:
|
| 18 |
"""Helper class for returning images from tools."""
|
| 19 |
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
path: Optional[Union[str, Path]] = None,
|
| 23 |
+
data: Optional[bytes] = None,
|
| 24 |
+
format: Optional[str] = None,
|
| 25 |
+
):
|
| 26 |
+
if path is None and data is None:
|
| 27 |
+
raise ValueError("Either path or data must be provided")
|
| 28 |
+
if path is not None and data is not None:
|
| 29 |
+
raise ValueError("Only one of path or data can be provided")
|
| 30 |
+
|
| 31 |
+
self.path = Path(path) if path else None
|
| 32 |
+
self.data = data
|
| 33 |
+
self._format = format
|
| 34 |
+
self._mime_type = self._get_mime_type()
|
| 35 |
+
|
| 36 |
+
def _get_mime_type(self) -> str:
|
| 37 |
+
"""Get MIME type from format or guess from file extension."""
|
| 38 |
+
if self._format:
|
| 39 |
+
return f"image/{self._format.lower()}"
|
| 40 |
+
|
| 41 |
+
if self.path:
|
| 42 |
+
suffix = self.path.suffix.lower()
|
| 43 |
+
return {
|
| 44 |
+
".png": "image/png",
|
| 45 |
+
".jpg": "image/jpeg",
|
| 46 |
+
".jpeg": "image/jpeg",
|
| 47 |
+
".gif": "image/gif",
|
| 48 |
+
".webp": "image/webp",
|
| 49 |
+
}.get(suffix, "application/octet-stream")
|
| 50 |
+
return "image/png" # default for raw binary data
|
| 51 |
|
| 52 |
def to_image_content(self) -> ImageContent:
|
| 53 |
"""Convert to MCP ImageContent."""
|
| 54 |
+
if self.path:
|
| 55 |
+
with open(self.path, "rb") as f:
|
| 56 |
+
data = base64.b64encode(f.read()).decode()
|
| 57 |
+
else:
|
| 58 |
+
data = base64.b64encode(self.data).decode()
|
| 59 |
+
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
| 60 |
|
| 61 |
|
| 62 |
class Tool(BaseModel):
|
uv.lock
CHANGED
|
@@ -222,7 +222,7 @@ wheels = [
|
|
| 222 |
|
| 223 |
[[package]]
|
| 224 |
name = "fastmcp"
|
| 225 |
-
version = "0.1.1.
|
| 226 |
source = { editable = "." }
|
| 227 |
dependencies = [
|
| 228 |
{ name = "httpx" },
|
|
|
|
| 222 |
|
| 223 |
[[package]]
|
| 224 |
name = "fastmcp"
|
| 225 |
+
version = "0.1.1.dev3+gbffea91.d20241130"
|
| 226 |
source = { editable = "." }
|
| 227 |
dependencies = [
|
| 228 |
{ name = "httpx" },
|