feat(mcp): add Pydantic tool schemas for free + premium MCP tools

New app/domains/mcp/tool_schemas.py defines input/output BaseModel
classes for all 7 free + premium MCP tools (search_news, resolve_wallet,
scan_token, check_bridge_transfers, defi_analytics, get_token_risk,
get_wallet_analysis). Includes TOOL_SCHEMAS registry mapping.

10 new tests validate all schemas instantiate correctly.
This commit is contained in:
Crypto Rug Munch 2026-07-08 05:27:42 +02:00
parent 416499bc30
commit e0a8ec8007
2 changed files with 232 additions and 0 deletions

View file

@ -0,0 +1,161 @@
"""Pydantic schemas for all MCP tools in the RMI registry.
Unified input/output validation for free, premium, and internal tools.
Used by the MCP registry, manifest generator, and API documentation
to enforce shape consistency across the tool mesh.
Each tool gets one Input and one Output model.
Free tools prefixed with 'Free', premium with 'Premium'.
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
# ── Free MCP Tools ───────────────────────────────────────────────
class FreeSearchNewsInput(BaseModel):
query: str = Field(..., description="Search term across 500+ crypto news sources")
limit: int = Field(default=10, ge=1, le=100, description="Max results")
fingerprint: str = Field(default="anon", description="Trial tracking ID")
class FreeSearchNewsArticle(BaseModel):
title: str
source: str
url: str | None = None
published: str | None = None
class FreeSearchNewsOutput(BaseModel):
results: list[FreeSearchNewsArticle] = Field(default_factory=list)
total: int = 0
class FreeResolveWalletInput(BaseModel):
address: str = Field(..., min_length=26, description="Crypto address across 13+ chains")
fingerprint: str = Field(default="anon")
class FreeResolveWalletLabel(BaseModel):
label_name: str | None = None
label_category: str | None = None
source: str | None = None
is_sanctioned: bool = False
class FreeResolveWalletOutput(BaseModel):
address: str
labels: list[FreeResolveWalletLabel] = Field(default_factory=list)
chains_found: list[str] = Field(default_factory=list)
class FreeScanTokenInput(BaseModel):
address: str = Field(..., min_length=26)
chain: str = Field(
default="ethereum",
description="solana, ethereum, base, arbitrum, bsc, polygon, etc.",
)
fingerprint: str = Field(default="anon")
class FreeScanTokenOutput(BaseModel):
address: str
chain: str
safety_score: int = Field(default=50, ge=0, le=100)
risk_flags: list[str] = Field(default_factory=list)
confidence: int = Field(default=0, ge=0, le=100)
modules_run: list[dict[str, Any]] = Field(default_factory=list)
class FreeBridgeCheckInput(BaseModel):
address: str = ""
bridge: str = Field(default="wormhole", description="wormhole | layerzero")
fingerprint: str = Field(default="anon")
class FreeBridgeCheckTransfer(BaseModel):
hash: str | None = None
status: str | None = None
timestamp: str | None = None
class FreeBridgeCheckOutput(BaseModel):
bridge: str
address: str
transfers: list[FreeBridgeCheckTransfer] = Field(default_factory=list)
class FreeDefiAnalyticsInput(BaseModel):
protocol: str = Field(default="", description="DeFi protocol slug (e.g. uniswap)")
chain: str = Field(default="")
fingerprint: str = Field(default="anon")
class FreeDefiAnalyticsOutput(BaseModel):
protocol: str
chain: str
tvl: float | None = None
protocols: list[dict[str, Any]] = Field(default_factory=list)
# ── Premium MCP Tools ────────────────────────────────────────────
class PremiumTokenRiskInput(BaseModel):
address: str = Field(..., min_length=26)
chain: str = Field(default="ethereum")
deep: bool = Field(default=False)
class PremiumTokenRiskOutput(BaseModel):
address: str
chain: str
score: int = Field(default=50, ge=0, le=100)
tier: str = Field(default="unknown")
factors: list[dict[str, Any]] = Field(default_factory=list)
class PremiumWalletAnalysisInput(BaseModel):
address: str = Field(..., min_length=26)
chain: str = Field(default="ethereum")
deep: bool = Field(default=False)
class PremiumWalletAnalysisOutput(BaseModel):
address: str
chain: str
score: int = Field(default=50, ge=0, le=100)
tags: list[str] = Field(default_factory=list)
# ── Registry mapping: tool name → (InputSchema, OutputSchema) ───
TOOL_SCHEMAS: dict[str, tuple[type[BaseModel], type[BaseModel]]] = {
"search_news": (FreeSearchNewsInput, FreeSearchNewsOutput),
"resolve_wallet": (FreeResolveWalletInput, FreeResolveWalletOutput),
"scan_token": (FreeScanTokenInput, FreeScanTokenOutput),
"check_bridge_transfers": (FreeBridgeCheckInput, FreeBridgeCheckOutput),
"defi_analytics": (FreeDefiAnalyticsInput, FreeDefiAnalyticsOutput),
"get_token_risk": (PremiumTokenRiskInput, PremiumTokenRiskOutput),
"get_wallet_analysis": (PremiumWalletAnalysisInput, PremiumWalletAnalysisOutput),
}
__all__ = [
"TOOL_SCHEMAS",
"FreeBridgeCheckInput",
"FreeBridgeCheckOutput",
"FreeDefiAnalyticsInput",
"FreeDefiAnalyticsOutput",
"FreeResolveWalletInput",
"FreeResolveWalletOutput",
"FreeScanTokenInput",
"FreeScanTokenOutput",
"FreeSearchNewsInput",
"FreeSearchNewsOutput",
"PremiumTokenRiskInput",
"PremiumTokenRiskOutput",
"PremiumWalletAnalysisInput",
"PremiumWalletAnalysisOutput",
]

View file

@ -0,0 +1,71 @@
"""Verify MCP tool schemas validate correctly."""
from pydantic import BaseModel
from app.domains.mcp.tool_schemas import (
TOOL_SCHEMAS,
FreeBridgeCheckInput,
FreeDefiAnalyticsInput,
FreeResolveWalletInput,
FreeScanTokenInput,
FreeSearchNewsInput,
FreeSearchNewsOutput,
PremiumTokenRiskInput,
PremiumWalletAnalysisInput,
)
class TestFreeToolSchemas:
def test_search_news_input(self):
m = FreeSearchNewsInput(query="bitcoin")
assert m.query == "bitcoin"
assert m.limit == 10
def test_search_news_output(self):
o = FreeSearchNewsOutput(
results=[{"title": "Test", "source": "CoinDesk"}],
total=1,
)
assert len(o.results) == 1
def test_resolve_wallet_input(self):
m = FreeResolveWalletInput(address="0x" + "a" * 40)
assert m.fingerprint == "anon"
def test_scan_token_input(self):
m = FreeScanTokenInput(address="So11111111111111111111111111111111111111112")
assert m.chain == "ethereum"
def test_bridge_check_input(self):
m = FreeBridgeCheckInput(bridge="wormhole")
assert m.bridge == "wormhole"
def test_defi_analytics_input(self):
m = FreeDefiAnalyticsInput(protocol="uniswap")
assert m.protocol == "uniswap"
class TestPremiumToolSchemas:
def test_token_risk_input(self):
m = PremiumTokenRiskInput(address="0x" + "b" * 40, deep=True)
assert m.deep is True
def test_wallet_analysis_input(self):
PremiumWalletAnalysisInput(address="0x" + "c" * 40)
class TestToolSchemasRegistry:
def test_all_tools_have_schemas(self):
for name in [
"search_news", "resolve_wallet", "scan_token",
"check_bridge_transfers", "defi_analytics",
"get_token_risk", "get_wallet_analysis",
]:
assert name in TOOL_SCHEMAS
def test_schema_types(self):
for _name, (in_schema, out_schema) in TOOL_SCHEMAS.items():
assert issubclass(in_schema, BaseModel)
assert issubclass(out_schema, BaseModel)