Files
rengine/mcp/tools/base.py
T
Yogesh OjhaandClaude Fable 5.1 61f57921a9 feat(remote-control): drive reNgine from a paired Telegram chat
A third transport over the MCP tool layer, beside HTTP and stdio. The
`channels` compose service long-polls the Bot API, so an instance needs
no public URL, certificate or open port; the listener runs on the api
image because MCP tools lazy-import app.services.

Commands are curated at the tool: a tool joins chat by declaring one
short `command` (/scan, /add, /vulns, /progress); no command means
MCP-only. Twenty-three commands in five groups, pinned by a test, with
replies rendered through ToolResult.blocks instead of the data dump.

An unknown chat gets a device code approved under Toolkit, Remote
control; anything above read is confirmed with the account's own
authenticator and capped at read when none is enrolled. The bot token is
the Telegram API key, shared with notifications. Replies are text plus
entities with UTF-16 offsets, never parse_mode, so target-written text
is literal by construction.

Also fixes Dimension.search passing scan_id= to a service that takes
scope, and gives query_assets an optional target for project-wide
search.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
2026-09-16 06:05:04 +05:30

61 lines
1.5 KiB
Python

"""The Tool contract."""
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import StrEnum
from typing import ClassVar
from pydantic import BaseModel, ConfigDict
from mcp.capabilities import Capability
from mcp.context import ToolContext
from mcp.result import ToolResult
class ToolInput(BaseModel):
"""Base for a tool's arguments."""
model_config = ConfigDict(extra="forbid")
class NoInput(ToolInput):
pass
class ToolGroup(StrEnum):
ORIENT = "Orient"
INTERROGATE = "Interrogate"
EXPLAIN = "Explain"
ACT = "Act"
class Tool(ABC):
"""One callable an agent can invoke."""
name: ClassVar[str]
title: ClassVar[str]
description: ClassVar[str]
capability: ClassVar[str] = Capability.READ.value
group: ClassVar[str] = ToolGroup.INTERROGATE.value
# the call destroys data a user cannot get back
destructive: ClassVar[bool] = False
# one short word; None keeps the tool out of chat
command: ClassVar[str | None] = None
# the field a bare chat argument fills
value_field: ClassVar[str] = ""
Input: ClassVar[type[ToolInput]] = NoInput
examples: ClassVar[tuple[str, ...]] = ()
@abstractmethod
async def run(self, ctx: ToolContext, args: ToolInput) -> ToolResult:
"""Answer the call."""
@classmethod
def schema(cls) -> dict:
schema = cls.Input.model_json_schema()
schema.pop("title", None)
schema.setdefault("type", "object")
schema.setdefault("properties", {})
return schema