Files
first_contas/mcp_servers/legacy_tim_mcp/main.py
2026-06-16 20:54:49 -03:00

658 lines
29 KiB
Python

from __future__ import annotations
import os
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Awaitable
from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl
import httpx
from dotenv import dotenv_values
from fastapi import FastAPI
from pydantic import BaseModel, Field
def _hydrate_env_from_dotenv_files() -> list[str]:
"""Load .env values even when the MCP server is started from another directory.
The previous package depended on `source .env` or Docker `env_file`. When the
user started uvicorn directly, variables like TIM_COMPLETE_INVOICES_URL were
absent and the MCP returned `Endpoint externo não configurado`.
This loader reads both the repository root .env and the MCP-local .env, and
fills only variables that are missing or exported as an empty string.
"""
here = Path(__file__).resolve()
candidates = []
for candidate in [
Path.cwd() / ".env",
here.parent / ".env",
here.parents[2] / ".env" if len(here.parents) > 2 else None,
]:
if candidate and candidate.exists() and candidate not in candidates:
candidates.append(candidate)
loaded: list[str] = []
for env_file in candidates:
values = dotenv_values(env_file)
for key, value in values.items():
if key and value is not None and not os.getenv(key):
os.environ[key] = str(value)
loaded.append(str(env_file))
return loaded
LOADED_ENV_FILES = _hydrate_env_from_dotenv_files()
APP_NAME = os.getenv("APP_NAME", "legacy-tim-mcp-server")
USE_MOCK = os.getenv("TIM_MCP_USE_MOCK", "false").lower() in {"1", "true", "yes", "y"}
DEFAULT_TIMEOUT = float(os.getenv("TIM_MCP_TIMEOUT_SECONDS", os.getenv("TIM_GATEWAY_DEFAULT_TIMEOUT", "30")))
DEFAULT_CLIENT_ID = os.getenv("TIM_CLIENT_ID", os.getenv("TIM_DEFAULT_CLIENT_ID", "AIAGENTCR"))
DEFAULT_USER_ID = os.getenv("TIM_USER_ID", "AIAGENTCR")
DEFAULT_CHANNEL = os.getenv("TIM_DEFAULT_CHANNEL", "APP")
DEFAULT_CSP_ID = os.getenv("TIM_DEFAULT_CSP_ID", "740")
def _first_env(*names: str, default: str = "") -> str:
for name in names:
value = os.getenv(name)
if value is not None and str(value).strip() != "":
return str(value).strip().strip('"').strip("'")
return default
@dataclass(frozen=True)
class EndpointConfig:
name: str
url: str
auth: str = ""
timeout: float = DEFAULT_TIMEOUT
client_id: str = DEFAULT_CLIENT_ID
extra_headers: dict[str, str] = field(default_factory=dict)
class GatewayError(RuntimeError):
"""Erro rico no padrão do legado: status, body, headers, request id e provider."""
def __init__(
self,
message: str,
*,
endpoint: str,
method: str,
url: str,
status_code: int | None = None,
error_code: str | None = None,
error_body: Any = None,
error_headers: dict[str, str] | None = None,
request_payload: Any = None,
response_text: str | None = None,
latency_ms: int | None = None,
exception_type: str = "GatewayError",
) -> None:
super().__init__(message)
self.endpoint = endpoint
self.method = method
self.url = url
self.status_code = status_code
self.error_code = error_code
self.error_body = error_body
self.error_headers = error_headers or {}
self.request_payload = request_payload
self.response_text = response_text
self.latency_ms = latency_ms
self.exception_type = exception_type
@property
def metadata(self) -> dict[str, Any]:
provider: dict[str, Any] = {}
if isinstance(self.error_body, dict) and isinstance(self.error_body.get("provider"), dict):
provider = self.error_body.get("provider") or {}
return {
"endpoint": self.endpoint,
"method": self.method,
"url": self.url,
"status_code": self.status_code,
"error_code": self.error_code,
"provider_service": provider.get("serviceName"),
"provider_error_code": provider.get("errorCode"),
"provider_error_message": provider.get("errorMessage"),
"message_id": _get_header_value(self.error_headers, "Messageid", "MessageId"),
"kong_request_id": _get_header_value(self.error_headers, "X-Kong-Request-Id"),
"latency_ms": self.latency_ms,
"exception_type": self.exception_type,
"request_payload_preview": _preview_json(self.request_payload),
"response_body_preview": _preview_json(self.error_body if self.error_body is not None else self.response_text),
"headers": _safe_headers(self.error_headers),
"mock": USE_MOCK,
}
def _preview_json(value: Any, limit: int = 2000) -> str:
if value is None:
return ""
text = str(value)
return text if len(text) <= limit else text[:limit] + "..."
def _get_header_value(headers: dict[str, str] | None, *keys: str) -> str:
normalized = {str(k).lower(): str(v) for k, v in (headers or {}).items()}
for key in keys:
value = normalized.get(key.lower(), "").strip()
if value:
return value
return ""
def _safe_headers(headers: dict[str, str] | None) -> dict[str, str]:
safe: dict[str, str] = {}
for k, v in (headers or {}).items():
lk = str(k).lower()
if lk in {"authorization", "authorizationoam", "authorization-oam", "proxy-authorization"} or "token" in lk or "secret" in lk:
safe[k] = "***" if v else ""
else:
safe[k] = str(v)
return safe
def _append_path(url: str, *parts: str) -> str:
base = url.rstrip("/")
suffix = "/".join(str(p).strip("/") for p in parts if str(p or "").strip("/"))
return f"{base}/{suffix}" if suffix else base
def _add_query(url: str, params: dict[str, Any]) -> str:
clean = {k: v for k, v in params.items() if v is not None and str(v) != ""}
if not clean:
return url
parts = urlsplit(url)
query = dict(parse_qsl(parts.query, keep_blank_values=True))
query.update(clean)
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
ENDPOINTS: dict[str, tuple[tuple[str, ...], tuple[str, ...], str, tuple[str, ...]]] = {
"TIM_COMPLETE_INVOICES": (("TIM_COMPLETE_INVOICES_URL",), ("TIM_COMPLETE_INVOICES_AUTH",), "", ("TIM_COMPLETE_INVOICES_TIMEOUT",)),
"TIM_PROFILE_BILL": (("TIM_PROFILE_BILL_URL", "TIM_URL_PERFIL_FATURA"), ("TIM_PROFILE_BILL_AUTH", "TIM_COMPLETE_INVOICES_AUTH"), "", ("TIM_PROFILE_BILL_TIMEOUT",)),
"TIM_CONTRATO": (("TIM_CONTRATO_URL",), ("TIM_CONTRATO_AUTH",), "", ("TIM_CONTRATO_TIMEOUT",)),
"TIM_QUERY_VAS": (("TIM_QUERY_VAS_URL", "TIM_URL_CONSULTA_VAS", "TIM_CONSULTA_URL"), ("TIM_QUERY_VAS_AUTH", "TIM_CONSULTA_AUTH"), "", ("TIM_QUERY_VAS_TIMEOUT", "TIM_CONSULTA_TIMEOUT")),
"TIM_BILL_PDF": (("TIM_BILL_PDF_URL", "TIM_URL_INVOICE_RECOVER"), ("TIM_BILL_PDF_AUTH", "TIM_INVOICE_RECOVER_AUTH"), "", ("TIM_BILL_PDF_TIMEOUT", "TIM_INVOICE_RECOVER_TIMEOUT")),
"TIM_PROTOCOL": (("TIM_PROTOCOL_URL",), ("TIM_PROTOCOL_AUTH",), "", ("TIM_PROTOCOL_TIMEOUT",)),
"TIM_CUSTOMER_CONTESTATION": (("TIM_CUSTOMER_CONTESTATION_URL", "TIM_DIVERGENCIA_URL"), ("TIM_CUSTOMER_CONTESTATION_AUTH", "TIM_DIVERGENCIA_AUTH"), "", ("TIM_CUSTOMER_CONTESTATION_TIMEOUT",)),
"TIM_SERVICE_REQUEST_STATUS": (("TIM_SERVICE_REQUEST_STATUS_URL",), ("TIM_SERVICE_REQUEST_STATUS_AUTH",), "", ("TIM_SERVICE_REQUEST_STATUS_TIMEOUT",)),
"TIM_CANCEL_VAS": (("TIM_CANCEL_VAS_URL", "TIM_CANCELLATION_URL", "TIM_CANCELAMENTO_URL"), ("TIM_CANCEL_VAS_AUTH", "TIM_CANCELLATION_AUTH", "TIM_CANCELAMENTO_AUTH"), "", ("TIM_CANCEL_VAS_TIMEOUT", "TIM_CANCELAMENTO_TIMEOUT")),
"TIM_BLOCK_VAS": (("TIM_BLOCK_VAS_URL", "TIM_URL_BLOQUEIO_VAS", "TIM_BLOQUEIO_URL"), ("TIM_BLOCK_VAS_AUTH", "TIM_BLOQUEIO_AUTH"), "", ("TIM_BLOCK_VAS_TIMEOUT", "TIM_BLOQUEIO_TIMEOUT")),
"TIM_SMS": (("TIM_SMS_URL",), ("TIM_SMS_AUTH", "SMS_BARCODE_AUTH"), "", ("TIM_SMS_TIMEOUT",)),
"TIM_TRACKING_ACTIVITIES": (("TIM_TRACKING_ACTIVITIES_URL",), ("TIM_TRACKING_ACTIVITIES_AUTH",), "", ("TIM_TRACKING_ACTIVITIES_TIMEOUT",)),
}
def endpoint_config(name: str) -> EndpointConfig:
url_names, auth_names, default_url, timeout_names = ENDPOINTS[name]
timeout_raw = _first_env(*timeout_names, default=str(DEFAULT_TIMEOUT))
try:
timeout = float(timeout_raw)
except ValueError:
timeout = DEFAULT_TIMEOUT
return EndpointConfig(
name=name,
url=_first_env(*url_names, default=default_url),
auth=_first_env(*auth_names, "TIM_DEFAULT_AUTH", default=""),
timeout=timeout,
client_id=_first_env(f"{name}_CLIENT_ID", "TIM_CLIENT_ID", "TIM_DEFAULT_CLIENT_ID", default=DEFAULT_CLIENT_ID),
)
class HttpApiGateway:
async def request(
self,
method: str,
config: EndpointConfig,
*,
url: str | None = None,
payload: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
) -> dict[str, Any]:
if not config.url:
raise GatewayError(
f"Endpoint externo não configurado para {config.name}",
endpoint=config.name,
method=method,
url=url or "",
request_payload=payload,
exception_type="MissingEndpoint",
)
target_url = url or config.url
request_headers = {"Accept": "application/json", **(headers or {})}
if config.auth and "Authorization" not in request_headers:
request_headers["Authorization"] = config.auth
if payload is not None:
request_headers.setdefault("Content-Type", "application/json")
t0 = time.monotonic()
try:
async with httpx.AsyncClient(timeout=config.timeout) as client:
resp = await client.request(method, target_url, json=payload if method.upper() != "GET" else None, headers=request_headers)
latency_ms = int((time.monotonic() - t0) * 1000)
parsed: Any
try:
parsed = resp.json()
except Exception:
parsed = {"raw_text": resp.text}
metadata = {
"endpoint": config.name,
"method": method,
"url": target_url,
"status_code": resp.status_code,
"latency_ms": latency_ms,
"request_headers": _safe_headers(request_headers),
"response_headers": _safe_headers(dict(resp.headers)),
"request_payload_preview": _preview_json(payload),
"response_body_preview": _preview_json(parsed),
"mock": False,
}
if resp.status_code >= 400:
code = None
if isinstance(parsed, dict):
code = parsed.get("error_code") or parsed.get("code") or parsed.get("errorCode") or f"HTTP_{resp.status_code}"
raise GatewayError(
f"{config.name} falhou: status={resp.status_code} body={_preview_json(parsed, 500)}",
endpoint=config.name,
method=method,
url=target_url,
status_code=resp.status_code,
error_code=str(code or f"HTTP_{resp.status_code}"),
error_body=parsed,
error_headers=dict(resp.headers),
request_payload=payload,
latency_ms=latency_ms,
exception_type="HTTPStatusError",
)
if isinstance(parsed, dict):
parsed.setdefault("metadata", metadata)
return parsed
return {"value": parsed, "metadata": metadata}
except GatewayError:
raise
except httpx.TimeoutException as exc:
latency_ms = int((time.monotonic() - t0) * 1000)
raise GatewayError(
f"{config.name} timeout após {config.timeout}s: {exc}",
endpoint=config.name,
method=method,
url=target_url,
request_payload=payload,
latency_ms=latency_ms,
exception_type="TimeoutException",
) from exc
except httpx.RequestError as exc:
latency_ms = int((time.monotonic() - t0) * 1000)
raise GatewayError(
f"{config.name} erro de transporte: {exc}",
endpoint=config.name,
method=method,
url=target_url,
request_payload=payload,
latency_ms=latency_ms,
exception_type=exc.__class__.__name__,
) from exc
async def get(self, config: EndpointConfig, url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
return await self.request("GET", config, url=url, headers=headers)
async def post(self, config: EndpointConfig, payload: dict[str, Any], url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
print("===========================================================================")
print(config, payload)
print("===========================================================================")
return await self.request("POST", config, url=url, payload=payload, headers=headers)
async def delete(self, config: EndpointConfig, payload: dict[str, Any] | None = None, url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
return await self.request("DELETE", config, url=url, payload=payload, headers=headers)
def _common_headers(config: EndpointConfig, *, client_id: str | None = None) -> dict[str, str]:
h = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if config.auth:
h["Authorization"] = config.auth
cid = client_id or config.client_id or DEFAULT_CLIENT_ID
if cid:
h["ClientID"] = cid
h["clientId"] = cid
return h
def _arg(args: dict[str, Any], *names: str, default: Any = "") -> Any:
for name in names:
value = args.get(name)
if value is not None and str(value).strip() != "":
return value
return default
class LegacyTimCommand:
command_name = "LegacyTimCommand"
endpoint_name = ""
def __init__(self, args: dict[str, Any], gateway: HttpApiGateway) -> None:
self.args = args or {}
self.gateway = gateway
self.config = endpoint_config(self.endpoint_name)
async def execute(self) -> dict[str, Any]:
raise NotImplementedError
def envelope(self, raw: dict[str, Any], *, normalized: dict[str, Any] | None = None) -> dict[str, Any]:
meta = raw.get("metadata", {}) if isinstance(raw, dict) else {}
return {
"command": self.command_name,
"endpoint": self.endpoint_name,
"ok": True,
"data": raw,
"normalized": normalized or {},
"metadata": {**meta, "command": self.command_name, "endpoint": self.endpoint_name, "mock": USE_MOCK},
}
class CompleteInvoicesCommand(LegacyTimCommand):
command_name = "CompleteInvoicesCommand"
endpoint_name = "TIM_COMPLETE_INVOICES"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
payload = {"msisdn": msisdn}
invoice_id = _arg(self.args, "invoice_id", "invoiceId", "contract_key")
if invoice_id:
payload["invoiceId"] = str(invoice_id)
raw = await self.gateway.post(self.config, payload, headers=_common_headers(self.config, client_id=_first_env("TIM_COMPLETE_INVOICES_CLIENT_ID", default=DEFAULT_CLIENT_ID)))
items = raw.get("paymentItems") if isinstance(raw, dict) else None
selected = None
if isinstance(items, list) and invoice_id:
for item in items:
if isinstance(item, dict) and str(item.get("id") or item.get("invoiceId") or item.get("number") or "") == str(invoice_id):
selected = item
break
if selected is None and isinstance(items, list) and items:
selected = next((i for i in items if isinstance(i, dict)), None)
normalized = {
"msisdn": msisdn,
"invoice_id": invoice_id or (selected or {}).get("id") or (selected or {}).get("invoiceId"),
"amount": (selected or {}).get("amount") or (selected or {}).get("totalAmount") or raw.get("totalAmount") if isinstance(raw, dict) else None,
"due_date": (selected or {}).get("dueDate") or (selected or {}).get("expirationDate"),
"status": (selected or {}).get("status"),
"payment_items_count": len(items) if isinstance(items, list) else None,
}
return self.envelope(raw, normalized=normalized)
class ProfileBillCommand(CompleteInvoicesCommand):
command_name = "ProfileBillCommand"
endpoint_name = "TIM_PROFILE_BILL"
class QueryVasCommand(LegacyTimCommand):
command_name = "QueryVasCommand"
endpoint_name = "TIM_QUERY_VAS"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
url = _append_path(self.config.url, msisdn)
url = _add_query(url, {"clientid": _first_env("TIM_CONSULTA_CLIENT_ID", "TIM_CLIENT_ID", default=DEFAULT_CLIENT_ID)})
raw = await self.gateway.get(self.config, url=url, headers=_common_headers(self.config))
services = raw.get("services") or raw.get("items") or raw.get("products") if isinstance(raw, dict) else None
normalized = {"msisdn": msisdn, "service_count": len(services) if isinstance(services, list) else None}
return self.envelope(raw, normalized=normalized)
class ContractInformationCommand(LegacyTimCommand):
command_name = "ContractInformationCommand"
endpoint_name = "TIM_CONTRATO"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
url = _append_path(self.config.url, msisdn)
raw = await self.gateway.get(self.config, url=url, headers=_common_headers(self.config))
return self.envelope(raw, normalized={"msisdn": msisdn, "plan": raw.get("plan") if isinstance(raw, dict) else None})
class InvoiceRecoverCommand(LegacyTimCommand):
command_name = "InvoiceRecoverCommand"
endpoint_name = "TIM_BILL_PDF"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
invoice_id = str(_arg(self.args, "invoice_id", "invoiceId", "contract_key"))
url = _add_query(self.config.url, {"invoiceId": invoice_id, "msisdn": msisdn, "customerId": _arg(self.args, "customer_id", "customerId")})
raw = await self.gateway.get(self.config, url=url, headers=_common_headers(self.config, client_id=_first_env("TIM_INVOICE_RECOVER_CLIENT_ID", "TIM_BILL_PDF_CLIENT_ID", default="TIMX")))
return self.envelope(raw, normalized={"msisdn": msisdn, "invoice_id": invoice_id, "pdf_available": bool(raw)})
class ProtocolCommand(LegacyTimCommand):
command_name = "ProtocolCommand"
endpoint_name = "TIM_PROTOCOL"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
payload = {
"customer": {"msisdn": msisdn},
"protocolNumber": _arg(self.args, "protocol_number", "protocolNumber"),
"reason": _arg(self.args, "reason", default="Atendimento Agent Contas FIRST"),
"channel": _arg(self.args, "channel", default=DEFAULT_CHANNEL),
"interactionCallId": _arg(self.args, "interaction_call_id", "ura_call_id", "message_id"),
}
raw = await self.gateway.post(self.config, payload, headers=_common_headers(self.config))
return self.envelope(raw, normalized={"msisdn": msisdn, "protocol_number": raw.get("protocolNumber") or payload["protocolNumber"] if isinstance(raw, dict) else payload["protocolNumber"]})
class CustomerContestationCommand(LegacyTimCommand):
command_name = "CustomerContestationCommand"
endpoint_name = "TIM_CUSTOMER_CONTESTATION"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
items = _arg(self.args, "items", default=[])
if not isinstance(items, list):
items = [items]
payload = {
"customer": {"msisdn": msisdn},
"invoice": {"number": _arg(self.args, "invoice_id", "invoice_number", "contract_key")},
"contestation": {
"reason": _arg(self.args, "reason", default="Contestação solicitada pelo atendimento"),
"claimedAmount": _arg(self.args, "amount", "claimed_amount", default=0),
"items": items,
},
"user": {"login": _first_env("TIM_CUSTOMER_CONTESTATION_USER_ID", default=DEFAULT_USER_ID)},
"clientId": _first_env("TIM_CUSTOMER_CONTESTATION_CLIENT_ID", default=DEFAULT_CLIENT_ID),
}
raw = await self.gateway.post(self.config, payload, headers=_common_headers(self.config, client_id=payload["clientId"]))
return self.envelope(raw, normalized={"msisdn": msisdn, "sr": raw.get("sr") or raw.get("serviceRequest") if isinstance(raw, dict) else None})
class ServiceRequestStatusCommand(LegacyTimCommand):
command_name = "ServiceRequestStatusCommand"
endpoint_name = "TIM_SERVICE_REQUEST_STATUS"
async def execute(self) -> dict[str, Any]:
protocol = _arg(self.args, "sr", "protocol_number", "protocolNumber", "service_request")
msisdn = _arg(self.args, "msisdn", "customer_id")
url = _add_query(self.config.url, {"protocolNumber": protocol, "serviceRequest": protocol, "msisdn": msisdn})
raw = await self.gateway.get(self.config, url=url, headers=_common_headers(self.config))
return self.envelope(raw, normalized={"protocol_number": protocol, "status": raw.get("status") if isinstance(raw, dict) else None})
class CancelVasCommand(LegacyTimCommand):
command_name = "CancelVasCommand"
endpoint_name = "TIM_CANCEL_VAS"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
service_id = _arg(self.args, "service_id", "app_id", "product_id")
payload = {"msisdn": msisdn, "serviceId": service_id, "cspId": _arg(self.args, "csp_id", default=DEFAULT_CSP_ID)}
raw = await self.gateway.delete(self.config, payload, headers=_common_headers(self.config))
return self.envelope(raw, normalized={"msisdn": msisdn, "service_id": service_id, "cancelled": True})
class BlockVasCommand(LegacyTimCommand):
command_name = "BlockVasCommand"
endpoint_name = "TIM_BLOCK_VAS"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
payload = {
"msisdn": msisdn,
"operationType": _first_env("TIM_BLOCK_VAS_OPERATION_TYPE", "TIM_BLOQUEIO_OPERATION_TYPE", default="block"),
"cspId": _arg(self.args, "csp_id", default=DEFAULT_CSP_ID),
"serviceId": _arg(self.args, "service_id", "app_id", "product_id"),
}
headers = _common_headers(self.config)
accept_encoding = _first_env("TIM_BLOCK_VAS_ACCEPT_ENCODING", "TIM_BLOQUEIO_ACCEPT_ENCODING", default="")
if accept_encoding:
headers["Accept-Encoding"] = accept_encoding
raw = await self.gateway.post(self.config, payload, headers=headers)
return self.envelope(raw, normalized={"msisdn": msisdn, "blocked": True})
class SmsCommand(LegacyTimCommand):
command_name = "SmsCommand"
endpoint_name = "TIM_SMS"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
payload = {
"msisdn": msisdn,
"message": _arg(self.args, "message", default=""),
"longUrl": _arg(self.args, "long_url", "longUrl", default=""),
"notifyUrl": _arg(self.args, "notify_url", "notifyUrl", default=""),
}
raw = await self.gateway.post(self.config, payload, headers=_common_headers(self.config, client_id=_first_env("TIM_SMS_CLIENT_ID", default="")))
return self.envelope(raw, normalized={"msisdn": msisdn, "sent": True})
class TrackingActivitiesCommand(LegacyTimCommand):
command_name = "TrackingActivitiesCommand"
endpoint_name = "TIM_TRACKING_ACTIVITIES"
async def execute(self) -> dict[str, Any]:
msisdn = str(_arg(self.args, "msisdn", "customer_id", "customer_key"))
payload = {
"channel": _first_env("TIM_TRACKING_ACTIVITIES_CHANNEL", default=DEFAULT_CHANNEL),
"customer": {"msisdn": msisdn},
"protocolNumber": _arg(self.args, "protocol_number", "protocolNumber"),
"invoice": {"number": _arg(self.args, "invoice_id", "invoice_number")},
"activity": {
"type": _arg(self.args, "activity_type", default="ATENDIMENTO"),
"status": _arg(self.args, "activity_status", default="OPEN"),
"id": _arg(self.args, "activity_id", default=""),
},
"user": {"login": _first_env("TIM_TRACKING_ACTIVITIES_USER_LOGIN", default=DEFAULT_USER_ID)},
}
raw = await self.gateway.post(self.config, payload, headers=_common_headers(self.config, client_id=_first_env("TIM_TRACKING_ACTIVITIES_CLIENT_ID", default=DEFAULT_CLIENT_ID)))
return self.envelope(raw, normalized={"msisdn": msisdn, "tracked": True})
COMMANDS: dict[str, type[LegacyTimCommand]] = {
"consultar_fatura": CompleteInvoicesCommand,
"consultar_pagamentos": ProfileBillCommand,
"consultar_plano": ContractInformationCommand,
"listar_servicos": QueryVasCommand,
"recuperar_pdf_fatura": InvoiceRecoverCommand,
"registrar_protocolo": ProtocolCommand,
"abrir_contestacao": CustomerContestationCommand,
"consultar_status_sr": ServiceRequestStatusCommand,
"atualizar_status_sr": ServiceRequestStatusCommand,
"cancelar_vas": CancelVasCommand,
"bloquear_vas": BlockVasCommand,
"enviar_sms": SmsCommand,
"registrar_tracking": TrackingActivitiesCommand,
}
MOCKS: dict[str, Callable[[dict[str, Any]], dict[str, Any]]] = {
"consultar_fatura": lambda a: {"paymentItems": [{"id": _arg(a, "invoice_id", default="3000131180"), "amount": 123.45, "dueDate": "2026-06-20", "status": "em aberto"}]},
"consultar_pagamentos": lambda a: {"payments": []},
"consultar_plano": lambda a: {"plan": "TIM Controle Smart"},
"listar_servicos": lambda a: {"services": []},
}
class ToolCall(BaseModel):
tool_name: str = Field(..., alias="tool_name")
arguments: dict[str, Any] = Field(default_factory=dict)
app = FastAPI(title=APP_NAME)
@app.get("/health")
async def health() -> dict[str, Any]:
configured = {name: bool(endpoint_config(name).url) for name in ENDPOINTS}
urls = {name: endpoint_config(name).url for name in ENDPOINTS}
return {
"ok": True,
"app": APP_NAME,
"mock": USE_MOCK,
"tool_count": len(COMMANDS),
"loaded_env_files": LOADED_ENV_FILES,
"configured_endpoints": configured,
"endpoint_urls": urls,
}
@app.get("/debug/config")
async def debug_config() -> dict[str, Any]:
return {
"loaded_env_files": LOADED_ENV_FILES,
"mock": USE_MOCK,
"endpoints": {
name: {
"url": endpoint_config(name).url,
"has_auth": bool(endpoint_config(name).auth),
"timeout": endpoint_config(name).timeout,
"client_id": endpoint_config(name).client_id,
}
for name in ENDPOINTS
},
}
@app.get("/mcp/tools/list")
async def list_tools() -> dict[str, Any]:
return {"tools": [{"name": name, "description": cls.command_name} for name, cls in COMMANDS.items()]}
@app.post("/mcp/tools/call")
async def call_tool(call: ToolCall) -> dict[str, Any]:
base_metadata = {"server": "legacy_tim", "tool": call.tool_name, "mock": USE_MOCK}
command_cls = COMMANDS.get(call.tool_name)
if command_cls is None:
return {"ok": False, "result": None, "error": f"Tool não encontrada: {call.tool_name}", "metadata": {**base_metadata, "exception_type": "ToolNotFound"}}
try:
if USE_MOCK:
raw = MOCKS.get(call.tool_name, lambda a: {"mock": True})(call.arguments or {})
result = {"command": command_cls.command_name, "ok": True, "data": raw, "metadata": {**base_metadata, "command": command_cls.command_name, "endpoint": command_cls.endpoint_name}}
else:
result = await command_cls(call.arguments or {}, HttpApiGateway()).execute()
metadata = dict(result.get("metadata") or {}) if isinstance(result, dict) else {}
ok = bool(result.get("ok", True)) if isinstance(result, dict) else True
error = str(result.get("error") or "") if isinstance(result, dict) else ""
return {"ok": ok, "result": result, "error": error or None, "metadata": {**base_metadata, **metadata}}
except GatewayError as exc:
return {
"ok": False,
"result": None,
"error": str(exc) or exc.exception_type,
"metadata": {**base_metadata, **exc.metadata},
}
except Exception as exc:
return {
"ok": False,
"result": None,
"error": str(exc) or exc.__class__.__name__,
"metadata": {**base_metadata, "exception_type": exc.__class__.__name__},
}