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:
parent
416499bc30
commit
e0a8ec8007
2 changed files with 232 additions and 0 deletions
161
app/domains/mcp/tool_schemas.py
Normal file
161
app/domains/mcp/tool_schemas.py
Normal 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",
|
||||
]
|
||||
71
tests/unit/domains/mcp/test_tool_schemas.py
Normal file
71
tests/unit/domains/mcp/test_tool_schemas.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue