Jeremiah Lowin commited on
Commit
0d751b2
·
1 Parent(s): a0bed44

Improve handling of editable installs

Browse files
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
- # Load dependencies from current directory's pyproject.toml
88
- fastmcp dev your_server.py --uv-directory .
89
 
90
  # Install additional packages
91
- fastmcp dev your_server.py --with pandas,numpy
92
 
93
  # Combine both
94
- fastmcp dev your_server.py --uv-directory . --with pandas,numpy
95
  ```
96
 
97
- 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.
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
- uv_directory: Optional[Path] = None,
 
 
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
- uv_directory: Optional directory containing pyproject.toml
 
 
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
- logger.warning(
58
- f"Server '{name}' already exists in Claude config",
 
 
 
 
 
 
 
59
  extra={"config_file": str(config_file)},
60
  )
61
- return False
62
 
63
  # Build uv run command
64
- args = []
65
- if uv_directory:
66
- args.extend(["--directory", str(uv_directory)])
67
- args.extend(["run", str(file)])
 
 
 
 
 
 
 
 
 
 
 
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
- uv_directory: Optional[Path] = None,
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
- uv_directory: Annotated[
149
  Optional[Path],
150
  typer.Option(
151
- "--uv-directory",
152
- "-d",
153
- help="Directory containing pyproject.toml (defaults to current directory)",
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
- "uv_directory": str(uv_directory) if uv_directory else None,
176
  "with_packages": with_packages,
177
  },
178
  )
179
 
180
  try:
181
- uv_cmd = _build_uv_command(file, uv_directory, with_packages)
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
- uv_directory: Annotated[
221
  Optional[Path],
222
  typer.Option(
223
- "--uv-directory",
224
- "-d",
225
- help="Directory containing pyproject.toml (defaults to current directory)",
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
- "uv_directory": str(uv_directory) if uv_directory else None,
242
  },
243
  )
244
 
@@ -278,17 +278,32 @@ def install(
278
  help="Custom name for the server (defaults to file name)",
279
  ),
280
  ] = None,
281
- uv_directory: Annotated[
282
  Optional[Path],
283
  typer.Option(
284
- "--uv-directory",
285
- "-d",
286
- help="Directory containing pyproject.toml (defaults to current directory)",
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
- "uv_directory": str(uv_directory) if uv_directory else None,
 
 
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(file, server_name, uv_directory=uv_directory):
 
 
 
 
 
 
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__(self, path: Union[str, Path], mime_type: Optional[str] = None):
21
- self.path = Path(path)
22
- self.mime_type = mime_type or self._guess_mime_type()
23
-
24
- def _guess_mime_type(self) -> str:
25
- """Guess MIME type from file extension."""
26
- suffix = self.path.suffix.lower()
27
- return {
28
- ".png": "image/png",
29
- ".jpg": "image/jpeg",
30
- ".jpeg": "image/jpeg",
31
- ".gif": "image/gif",
32
- ".webp": "image/webp",
33
- }.get(suffix, "application/octet-stream")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  def to_image_content(self) -> ImageContent:
36
  """Convert to MCP ImageContent."""
37
- with open(self.path, "rb") as f:
38
- data = base64.b64encode(f.read()).decode()
39
- return ImageContent(type="image", data=data, mimeType=self.mime_type)
 
 
 
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.dev1+gca7438f.d20241130"
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" },