Spaces:
Running
Running
Merge pull request #43 from justjoehere/windows2
Browse filesAdditional Windows Fixes for Dev running and for importing modules in a server
- Windows_Notes.md +43 -0
- src/fastmcp/cli/cli.py +40 -5
- tests/test_cli.py +48 -13
Windows_Notes.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
# Getting your development environment set up properly
|
|
|
|
| 2 |
```bash
|
| 3 |
uv venv
|
| 4 |
.venv\Scripts\activate
|
| 5 |
uv pip install -e ".[dev]"
|
| 6 |
```
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
# Fixing `AttributeError: module 'collections' has no attribute 'Callable'`
|
| 9 |
- open `.venv\Lib\site-packages\pyreadline\py3k_compat.py`
|
| 10 |
- change `return isinstance(x, collections.Callable)` to
|
|
@@ -13,3 +17,42 @@ from collections.abc import Callable
|
|
| 13 |
return isinstance(x, Callable)
|
| 14 |
```
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Getting your development environment set up properly
|
| 2 |
+
To get your environment up and running properly, you'll need a slightly different set of commands that are windows specific:
|
| 3 |
```bash
|
| 4 |
uv venv
|
| 5 |
.venv\Scripts\activate
|
| 6 |
uv pip install -e ".[dev]"
|
| 7 |
```
|
| 8 |
|
| 9 |
+
This will install the package in editable mode, and install the development dependencies.
|
| 10 |
+
|
| 11 |
+
|
| 12 |
# Fixing `AttributeError: module 'collections' has no attribute 'Callable'`
|
| 13 |
- open `.venv\Lib\site-packages\pyreadline\py3k_compat.py`
|
| 14 |
- change `return isinstance(x, collections.Callable)` to
|
|
|
|
| 17 |
return isinstance(x, Callable)
|
| 18 |
```
|
| 19 |
|
| 20 |
+
# Helpful notes
|
| 21 |
+
For developing FastMCP
|
| 22 |
+
## Install local development version of FastMCP into a local FastMCP project server
|
| 23 |
+
- ensure
|
| 24 |
+
- change directories to your FastMCP Server location so you can install it in your .venv
|
| 25 |
+
- run `.venv\Scripts\activate` to activate your virtual environment
|
| 26 |
+
- Then run a series of commands to uninstall the old version and install the new
|
| 27 |
+
```bash
|
| 28 |
+
# First uninstall
|
| 29 |
+
uv pip uninstall fastmcp
|
| 30 |
+
|
| 31 |
+
# Clean any build artifacts in your fastmcp directory
|
| 32 |
+
cd C:\path\to\fastmcp
|
| 33 |
+
del /s /q *.egg-info
|
| 34 |
+
|
| 35 |
+
# Then reinstall in your weather project
|
| 36 |
+
cd C:\path\to\new\fastmcp_server
|
| 37 |
+
uv pip install --no-cache-dir -e C:\Users\justj\PycharmProjects\fastmcp
|
| 38 |
+
|
| 39 |
+
# Check that it installed properly and has the correct git hash
|
| 40 |
+
pip show fastmcp
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Running the FastMCP server with Inspector
|
| 44 |
+
MCP comes with a node.js application called Inspector that can be used to inspect the FastMCP server. To run the inspector, you'll need to install node.js and npm. Then you can run the following commands:
|
| 45 |
+
```bash
|
| 46 |
+
fastmcp dev server.py
|
| 47 |
+
```
|
| 48 |
+
This will launch a web app on http://localhost:5173/ that you can use to inspect the FastMCP server.
|
| 49 |
+
|
| 50 |
+
## If you start development before creating a fork - your get out of jail free card
|
| 51 |
+
- Add your fork as a new remote to your local repository `git remote add fork git@github.com:YOUR-USERNAME/REPOSITORY-NAME.git`
|
| 52 |
+
- This will add your repo, short named 'fork', as a remote to your local repository
|
| 53 |
+
- Verify that it was added correctly by running `git remote -v`
|
| 54 |
+
- Commit your changes
|
| 55 |
+
- Push your changes to your fork `git push fork <branch>`
|
| 56 |
+
- Create your pull request on GitHub
|
| 57 |
+
|
| 58 |
+
|
src/fastmcp/cli/cli.py
CHANGED
|
@@ -11,8 +11,8 @@ import typer
|
|
| 11 |
from typing_extensions import Annotated
|
| 12 |
import dotenv
|
| 13 |
|
| 14 |
-
from .
|
| 15 |
-
from . import
|
| 16 |
|
| 17 |
logger = get_logger("cli")
|
| 18 |
|
|
@@ -24,6 +24,22 @@ app = typer.Typer(
|
|
| 24 |
)
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def _parse_env_var(env_var: str) -> Tuple[str, str]:
|
| 28 |
"""Parse environment variable string in format KEY=VALUE."""
|
| 29 |
if "=" not in env_var:
|
|
@@ -99,6 +115,11 @@ def _import_server(file: Path, server_object: Optional[str] = None):
|
|
| 99 |
Returns:
|
| 100 |
The server object
|
| 101 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
# Import the module
|
| 103 |
spec = importlib.util.spec_from_file_location("server_module", file)
|
| 104 |
if not spec or not spec.loader:
|
|
@@ -205,10 +226,22 @@ def dev(
|
|
| 205 |
with_packages = list(set(with_packages + server.dependencies))
|
| 206 |
|
| 207 |
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
process = subprocess.run(
|
| 210 |
-
[
|
| 211 |
check=True,
|
|
|
|
| 212 |
)
|
| 213 |
sys.exit(process.returncode)
|
| 214 |
except subprocess.CalledProcessError as e:
|
|
@@ -223,7 +256,9 @@ def dev(
|
|
| 223 |
sys.exit(e.returncode)
|
| 224 |
except FileNotFoundError:
|
| 225 |
logger.error(
|
| 226 |
-
"npx not found. Please
|
|
|
|
|
|
|
| 227 |
extra={"file": str(file)},
|
| 228 |
)
|
| 229 |
sys.exit(1)
|
|
|
|
| 11 |
from typing_extensions import Annotated
|
| 12 |
import dotenv
|
| 13 |
|
| 14 |
+
from fastmcp.cli import claude
|
| 15 |
+
from fastmcp.utilities.logging import get_logger
|
| 16 |
|
| 17 |
logger = get_logger("cli")
|
| 18 |
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
|
| 27 |
+
def _get_npx_command():
|
| 28 |
+
"""Get the correct npx command for the current platform."""
|
| 29 |
+
if sys.platform == "win32":
|
| 30 |
+
# Try both npx.cmd and npx.exe on Windows
|
| 31 |
+
for cmd in ["npx.cmd", "npx.exe", "npx"]:
|
| 32 |
+
try:
|
| 33 |
+
subprocess.run(
|
| 34 |
+
[cmd, "--version"], check=True, capture_output=True, shell=True
|
| 35 |
+
)
|
| 36 |
+
return cmd
|
| 37 |
+
except subprocess.CalledProcessError:
|
| 38 |
+
continue
|
| 39 |
+
return None
|
| 40 |
+
return "npx" # On Unix-like systems, just use npx
|
| 41 |
+
|
| 42 |
+
|
| 43 |
def _parse_env_var(env_var: str) -> Tuple[str, str]:
|
| 44 |
"""Parse environment variable string in format KEY=VALUE."""
|
| 45 |
if "=" not in env_var:
|
|
|
|
| 115 |
Returns:
|
| 116 |
The server object
|
| 117 |
"""
|
| 118 |
+
# Add parent directory to Python path so imports can be resolved
|
| 119 |
+
file_dir = str(file.parent)
|
| 120 |
+
if file_dir not in sys.path:
|
| 121 |
+
sys.path.insert(0, file_dir)
|
| 122 |
+
|
| 123 |
# Import the module
|
| 124 |
spec = importlib.util.spec_from_file_location("server_module", file)
|
| 125 |
if not spec or not spec.loader:
|
|
|
|
| 226 |
with_packages = list(set(with_packages + server.dependencies))
|
| 227 |
|
| 228 |
uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)
|
| 229 |
+
|
| 230 |
+
# Get the correct npx command
|
| 231 |
+
npx_cmd = _get_npx_command()
|
| 232 |
+
if not npx_cmd:
|
| 233 |
+
logger.error(
|
| 234 |
+
"npx not found. Please ensure Node.js and npm are properly installed "
|
| 235 |
+
"and added to your system PATH."
|
| 236 |
+
)
|
| 237 |
+
sys.exit(1)
|
| 238 |
+
|
| 239 |
+
# Run the MCP Inspector command with shell=True on Windows
|
| 240 |
+
shell = sys.platform == "win32"
|
| 241 |
process = subprocess.run(
|
| 242 |
+
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
|
| 243 |
check=True,
|
| 244 |
+
shell=shell,
|
| 245 |
)
|
| 246 |
sys.exit(process.returncode)
|
| 247 |
except subprocess.CalledProcessError as e:
|
|
|
|
| 256 |
sys.exit(e.returncode)
|
| 257 |
except FileNotFoundError:
|
| 258 |
logger.error(
|
| 259 |
+
"npx not found. Please ensure Node.js and npm are properly installed "
|
| 260 |
+
"and added to your system PATH. You may need to restart your terminal "
|
| 261 |
+
"after installation.",
|
| 262 |
extra={"file": str(file)},
|
| 263 |
)
|
| 264 |
sys.exit(1)
|
tests/test_cli.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""Tests for the FastMCP CLI."""
|
| 2 |
|
| 3 |
import json
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
from unittest.mock import patch, call
|
| 6 |
|
|
@@ -276,7 +277,6 @@ def test_server_dependencies_empty(mock_config, server_file):
|
|
| 276 |
|
| 277 |
def test_dev_with_dependencies(mock_config, server_file):
|
| 278 |
"""Test that dev command handles dependencies correctly."""
|
| 279 |
-
# Create a server file with dependencies
|
| 280 |
server_file = server_file.parent / "server_with_deps.py"
|
| 281 |
server_file.write_text(
|
| 282 |
"""from fastmcp import FastMCP
|
|
@@ -287,21 +287,56 @@ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|
| 287 |
runner = CliRunner()
|
| 288 |
|
| 289 |
with patch("subprocess.run") as mock_run:
|
| 290 |
-
mock_run.return_value.returncode = 0
|
| 291 |
result = runner.invoke(app, ["dev", str(server_file)])
|
| 292 |
assert result.exit_code == 0
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
|
| 307 |
def test_run_with_dependencies(mock_config, server_file):
|
|
|
|
| 1 |
"""Tests for the FastMCP CLI."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
+
import sys
|
| 5 |
from pathlib import Path
|
| 6 |
from unittest.mock import patch, call
|
| 7 |
|
|
|
|
| 277 |
|
| 278 |
def test_dev_with_dependencies(mock_config, server_file):
|
| 279 |
"""Test that dev command handles dependencies correctly."""
|
|
|
|
| 280 |
server_file = server_file.parent / "server_with_deps.py"
|
| 281 |
server_file.write_text(
|
| 282 |
"""from fastmcp import FastMCP
|
|
|
|
| 287 |
runner = CliRunner()
|
| 288 |
|
| 289 |
with patch("subprocess.run") as mock_run:
|
| 290 |
+
mock_run.return_value.returncode = 0
|
| 291 |
result = runner.invoke(app, ["dev", str(server_file)])
|
| 292 |
assert result.exit_code == 0
|
| 293 |
|
| 294 |
+
if sys.platform == "win32":
|
| 295 |
+
# On Windows, expect two calls
|
| 296 |
+
assert mock_run.call_count == 2
|
| 297 |
+
assert mock_run.call_args_list[0] == call(
|
| 298 |
+
["npx.cmd", "--version"], check=True, capture_output=True, shell=True
|
| 299 |
+
)
|
| 300 |
+
assert mock_run.call_args_list[1] == call(
|
| 301 |
+
[
|
| 302 |
+
"npx.cmd",
|
| 303 |
+
"@modelcontextprotocol/inspector",
|
| 304 |
+
"uv",
|
| 305 |
+
"run",
|
| 306 |
+
"--with",
|
| 307 |
+
"fastmcp",
|
| 308 |
+
"--with",
|
| 309 |
+
"numpy",
|
| 310 |
+
"--with",
|
| 311 |
+
"pandas",
|
| 312 |
+
"fastmcp",
|
| 313 |
+
"run",
|
| 314 |
+
str(server_file),
|
| 315 |
+
],
|
| 316 |
+
check=True,
|
| 317 |
+
shell=True,
|
| 318 |
+
)
|
| 319 |
+
else:
|
| 320 |
+
# On Unix, expect one call
|
| 321 |
+
mock_run.assert_called_once_with(
|
| 322 |
+
[
|
| 323 |
+
"npx",
|
| 324 |
+
"@modelcontextprotocol/inspector",
|
| 325 |
+
"uv",
|
| 326 |
+
"run",
|
| 327 |
+
"--with",
|
| 328 |
+
"fastmcp",
|
| 329 |
+
"--with",
|
| 330 |
+
"numpy",
|
| 331 |
+
"--with",
|
| 332 |
+
"pandas",
|
| 333 |
+
"fastmcp",
|
| 334 |
+
"run",
|
| 335 |
+
str(server_file),
|
| 336 |
+
],
|
| 337 |
+
check=True,
|
| 338 |
+
shell=False, # Note: shell=False on Unix
|
| 339 |
+
)
|
| 340 |
|
| 341 |
|
| 342 |
def test_run_with_dependencies(mock_config, server_file):
|