mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 14:27:40 +01:00
Port the MCP server to mcp 2.x, and say what each tool is allowed to do
mcp 2.0.0 removed mcp.server.fastmcp; MCPServer is the same object under a new name, so @mcp.tool() and mcp.run() are untouched and every tool keeps its name, signature and returned shape. Verified against a clean 2.0.0 venv: all four tools list, and the whole suite -- including test_mcp.py, which could not even import while the dependency was broken -- passes at 100% coverage, 901 tests. Two things the port buys, neither of which existed in 1.x: * Tool annotations. The section banner here already said "state-only; never expose secrets, never mutate"; now the protocol says it too, as read-only, idempotent and closed-world hints, which is what lets a client run these without asking first. The day one of them starts writing, the annotation is what has to change with it. * Structured output. 2.x derives an output schema from the return types and sends structured content alongside the text, so a caller no longer re-parses JSON we already had. No code change -- it falls out of the type hints that were already there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
This commit is contained in:
parent
feb7c173d5
commit
a0a3fb0eb0
@ -101,7 +101,7 @@ repos:
|
|||||||
- pytest
|
- pytest
|
||||||
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.4.0#subdirectory=gatelock
|
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.4.0#subdirectory=gatelock
|
||||||
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync
|
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync
|
||||||
- mcp>=1.9,<2
|
- mcp>=2,<3
|
||||||
- pydantic
|
- pydantic
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
|
|||||||
@ -20,7 +20,7 @@ screen-locker-status = "screen_locker.status_view:main"
|
|||||||
# so the systemd/CLI system-python path stays dependency-light and never has
|
# so the systemd/CLI system-python path stays dependency-light and never has
|
||||||
# to import the MCP SDK.
|
# to import the MCP SDK.
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
mcp = ["mcp>=1.9,<2"]
|
mcp = ["mcp>=2,<3"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
# This repo also holds stronglift_replacement/ (an unrelated Flutter app, not
|
# This repo also holds stronglift_replacement/ (an unrelated Flutter app, not
|
||||||
|
|||||||
@ -5,7 +5,7 @@ codespell>=2.2.0
|
|||||||
coverage>=7.4.0
|
coverage>=7.4.0
|
||||||
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync
|
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync
|
||||||
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.4.0#subdirectory=gatelock
|
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.4.0#subdirectory=gatelock
|
||||||
mcp>=1.9,<2
|
mcp>=2,<3
|
||||||
mypy>=1.8.0
|
mypy>=1.8.0
|
||||||
pre-commit>=3.6.0
|
pre-commit>=3.6.0
|
||||||
pylint>=3.0.0
|
pylint>=3.0.0
|
||||||
|
|||||||
@ -37,7 +37,8 @@ from pathlib import Path
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server import MCPServer
|
||||||
|
from mcp.types import ToolAnnotations
|
||||||
|
|
||||||
from screen_locker._compliance_state import (
|
from screen_locker._compliance_state import (
|
||||||
has_logged_today,
|
has_logged_today,
|
||||||
@ -62,7 +63,18 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
mcp = FastMCP("screen-locker")
|
mcp = MCPServer("screen-locker")
|
||||||
|
|
||||||
|
# Every tool here only reads local state: the workout log, the sick history
|
||||||
|
# and the clock. Saying so lets a client run them without asking -- and the
|
||||||
|
# day one of them stops being read-only, this annotation is the thing that
|
||||||
|
# has to change with it. (See the section banner below: "never mutate" was
|
||||||
|
# already the rule; this is the machine-readable half of it.)
|
||||||
|
_READS_ONLY = ToolAnnotations(
|
||||||
|
read_only_hint=True,
|
||||||
|
idempotent_hint=True,
|
||||||
|
open_world_hint=False,
|
||||||
|
)
|
||||||
|
|
||||||
# The workout log lives beside the package, same default as
|
# The workout log lives beside the package, same default as
|
||||||
# ``_status_data._DEFAULT_LOG_FILE``; declared here to avoid importing a
|
# ``_status_data._DEFAULT_LOG_FILE``; declared here to avoid importing a
|
||||||
@ -75,7 +87,7 @@ _LOG_FILE = Path(__file__).resolve().parent / "workout_log.json"
|
|||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(title="Workout gate status", annotations=_READS_ONLY)
|
||||||
def get_status() -> dict[str, Any]:
|
def get_status() -> dict[str, Any]:
|
||||||
"""Return the full read-only status snapshot.
|
"""Return the full read-only status snapshot.
|
||||||
|
|
||||||
@ -87,7 +99,7 @@ def get_status() -> dict[str, Any]:
|
|||||||
return asdict(gather_status())
|
return asdict(gather_status())
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(title="One-line status summary", annotations=_READS_ONLY)
|
||||||
def get_summary() -> dict[str, Any]:
|
def get_summary() -> dict[str, Any]:
|
||||||
"""Return the one-line i3blocks summary plus the compliance-state word.
|
"""Return the one-line i3blocks summary plus the compliance-state word.
|
||||||
|
|
||||||
@ -102,7 +114,7 @@ def get_summary() -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(title="Why the screen is locked", annotations=_READS_ONLY)
|
||||||
def explain_lock() -> dict[str, Any]:
|
def explain_lock() -> dict[str, Any]:
|
||||||
"""Return why the screen is or is not being locked right now.
|
"""Return why the screen is or is not being locked right now.
|
||||||
|
|
||||||
@ -114,7 +126,7 @@ def explain_lock() -> dict[str, Any]:
|
|||||||
return asdict(gather_status().lock_explanation)
|
return asdict(gather_status().lock_explanation)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool(title="Lock-decision predicates", annotations=_READS_ONLY)
|
||||||
def get_flags() -> dict[str, bool]:
|
def get_flags() -> dict[str, bool]:
|
||||||
"""Return the individual boolean lock-decision predicates for today.
|
"""Return the individual boolean lock-decision predicates for today.
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user