mcp-tool-authoringlisted
Install: claude install-skill anrysys/tg-ai
# Authoring an MCP tool
Authority: [docs/10-product/srs.md](../../../docs/10-product/srs.md) and
[docs/30-api/mcp-tools.md](../../../docs/30-api/mcp-tools.md).
## The shape
```python
@mcp.tool()
@guarded_tool
async def tg_do_thing(target: str, limit: int = 10) -> str:
"""One line saying what this does for the user.
When to use it rather than a neighbouring tool, and any consequence the
agent must know about before calling.
Args:
target: What forms are accepted.
limit: What range; note that it is clamped, not rejected.
Returns:
What the text contains, and what a failure looks like.
"""
```
Decorator order matters: `@mcp.tool()` outermost, `@guarded_tool` directly on
the function.
## Hard requirements
| Rule | Why |
| --- | --- |
| Return `str` under all conditions; never raise | An escaping exception can kill the stdio server mid-session (`SPEC-SND-004`) |
| No `from __future__ import annotations` in `server.py` | FastMCP inspects signatures at import time and cannot resolve string annotations. It fails with `issubclass() arg 1 must be a class` |
| Clamp `limit`, never reject it | `max(1, min(int(limit), N))`. An agent guessing 10000 should get an answer, not a round trip (`SPEC-RCV-004`) |
| `raise ToolError(...)` for expected failures | Produces `ERROR: <message>` without stack-trace noise |
| Never `print()` | stdout is the protocol channel. Use `log.*` (stderr) |
| Acquire the client via `await telegram()`, the pool