mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-10 07:39:10 +02:00
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""Tests for the `fastmcp plugin` CLI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
|
|
def _run_fastmcp(
|
|
*args: str, cwd: Path, env_extra: dict[str, str] | None = None
|
|
) -> subprocess.CompletedProcess[str]:
|
|
"""Invoke the `fastmcp` CLI as a subprocess."""
|
|
env = os.environ.copy()
|
|
env["PYTHONPATH"] = str(cwd)
|
|
if env_extra:
|
|
env.update(env_extra)
|
|
return subprocess.run(
|
|
[sys.executable, "-m", "fastmcp.cli", *args],
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
|
|
|
|
class TestManifestCLI:
|
|
def test_manifest_for_top_level_class(self, tmp_path: Path):
|
|
(tmp_path / "demo.py").write_text(
|
|
textwrap.dedent(
|
|
"""
|
|
from fastmcp.server.plugins import Plugin, PluginMeta
|
|
|
|
class Demo(Plugin):
|
|
meta = PluginMeta(name="demo", version="0.1.0")
|
|
"""
|
|
)
|
|
)
|
|
result = _run_fastmcp("plugin", "manifest", "demo:Demo", cwd=tmp_path)
|
|
assert result.returncode == 0, result.stderr
|
|
manifest = json.loads(result.stdout)
|
|
assert manifest["name"] == "demo"
|
|
assert manifest["entry_point"] == "demo:Demo"
|
|
|
|
def test_manifest_for_nested_class(self, tmp_path: Path):
|
|
"""`__qualname__` produces dotted paths for nested classes; the CLI
|
|
must traverse the dots to resolve the inner class."""
|
|
(tmp_path / "demo.py").write_text(
|
|
textwrap.dedent(
|
|
"""
|
|
from fastmcp.server.plugins import Plugin, PluginMeta
|
|
|
|
class Outer:
|
|
class Inner(Plugin):
|
|
meta = PluginMeta(name="inner", version="0.1.0")
|
|
"""
|
|
)
|
|
)
|
|
result = _run_fastmcp("plugin", "manifest", "demo:Outer.Inner", cwd=tmp_path)
|
|
assert result.returncode == 0, result.stderr
|
|
manifest = json.loads(result.stdout)
|
|
assert manifest["name"] == "inner"
|
|
assert manifest["entry_point"] == "demo:Outer.Inner"
|
|
|
|
def test_manifest_emits_clean_error_for_invalid_meta(self, tmp_path: Path):
|
|
"""A plugin with invalid meta must produce a clean error, not a traceback."""
|
|
(tmp_path / "bad.py").write_text(
|
|
textwrap.dedent(
|
|
"""
|
|
from fastmcp.server.plugins import Plugin, PluginMeta
|
|
|
|
class Bad(Plugin):
|
|
meta = PluginMeta(
|
|
name="bad",
|
|
version="0.1.0",
|
|
dependencies=["not a valid pep508 spec!!"],
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
result = _run_fastmcp("plugin", "manifest", "bad:Bad", cwd=tmp_path)
|
|
assert result.returncode == 1
|
|
# Error goes through logger.error, not as a Python traceback.
|
|
assert "Traceback" not in result.stderr
|
|
assert "PEP 508" in result.stderr
|
|
|
|
def test_manifest_emits_clean_error_for_missing_module(self, tmp_path: Path):
|
|
result = _run_fastmcp(
|
|
"plugin", "manifest", "nonexistent_module:Thing", cwd=tmp_path
|
|
)
|
|
assert result.returncode == 1
|
|
assert "Traceback" not in result.stderr
|