1"""The `MCPTool` base class. Subclass, define `__init__` with your args, and implement `run()`."""
 2
 3from __future__ import annotations
 4
 5import inspect
 6from abc import ABC, abstractmethod
 7from typing import TYPE_CHECKING, Any
 8
 9from .schema import build_input_schema
10
11if TYPE_CHECKING:
12    from .views import MCPView
13
14
15class MCPTool(ABC):
16    """Base class for MCP tools.
17
18    Tools take their typed arguments through `__init__` (like a dataclass
19    or pydantic model) and execute via `run()`. Name, description, and
20    input schema are derived automatically:
21
22    - `name` — the class name (override with `name = "..."`)
23    - `description` — the class docstring (override with `description = "..."`)
24    - `input_schema` — derived from `__init__`'s typed signature
25      (override by setting `input_schema = {...}` for custom shapes)
26
27        class Greet(MCPTool):
28            '''Greet someone by name.'''
29
30            def __init__(self, name: str):
31                self.name = name
32
33            def run(self) -> str:
34                return f"Hello, {self.name}!"
35
36    Tool instances have `self.mcp` set by the dispatcher before `run()`
37    is called — use it to read `self.mcp.request`, `self.mcp.user`, etc.
38    Override `allowed_for(mcp)` (as a classmethod) to filter when the
39    tool is included — auth gating, feature flags, tenant checks. Filters
40    run *before* instantiation, so they can't depend on the caller's args.
41    """
42
43    name: str = ""
44    description: str = ""
45    input_schema: dict[str, Any] | None = None
46
47    # Set by the MCPView dispatcher before `run()` is called.
48    mcp: MCPView
49
50    def __init__(self) -> None:
51        """Default no-arg init — override in subclasses with your typed args."""
52
53    @abstractmethod
54    def run(self) -> Any:
55        """Execute the tool. Return a str, dict, list, or anything serializable."""
56
57    @classmethod
58    def allowed_for(cls, mcp: MCPView) -> bool:
59        """Return False to exclude this tool from `mcp`'s toolset.
60
61        Runs before the tool is instantiated, so implementations can only
62        rely on the MCPView's class-level state (e.g. `mcp.user`, request
63        headers, settings) — not on the caller's tool arguments.
64
65        Tools that return False are hidden from `tools/list` and rejected
66        from `tools/call` (as "unknown tool" — existence isn't leaked).
67        """
68        return True
69
70    def __init_subclass__(cls, **kwargs: Any) -> None:
71        super().__init_subclass__(**kwargs)
72        if not cls.__dict__.get("name"):
73            cls.name = cls.__name__
74        if not cls.__dict__.get("description"):
75            cls.description = inspect.cleandoc(cls.__doc__ or "")
76        if cls.__dict__.get("input_schema") is None:
77            cls.input_schema = build_input_schema(cls.__init__)