mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
Add new FastMCP Plugin support (#3970)
This commit is contained in:
parent
801385df44
commit
823ea4c5fc
8 changed files with 1643 additions and 14 deletions
95
tests/cli/test_plugin_cli.py
Normal file
95
tests/cli/test_plugin_cli.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue