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__)