Retirar el servidor MCP y fijar todas las dependencias del API

MCP: se retira por completo (endpoint, paquete api/app/mcp/ y dependencia).
Tres razones, en orden de peso:

- Nadie lo usa.
- Llevaba roto desde antes de este trabajo. Con credencial valida devolvia 500:
  el handler construia un StreamableHTTPServerTransport nuevo por peticion, sin
  manejo de sesion. Lo que estaba expuesto a internet era la puerta abierta de un
  cuarto averiado.
- Su SDK sin fijar tumbo el API entero en produccion al saltar a 2.0.0.

Retirarlo es una mitigacion mas fuerte que autenticarlo, que fue lo que hizo el
commit anterior. Recuperable con `git show edb500f5:api/app/mcp/server.py`.
Se limpia tambien la ruta "/mcp" que el endpoint raiz seguia anunciando, y las
menciones a MCP del docstring y la descripcion de Swagger.

Dependencias: once de las doce eran rangos `>=` sin techo, o sea que cada
reconstruccion era una tirada de dados contra PyPI. `pydantic>=2.0` habria
aceptado pydantic 3 con la misma alegria con la que `mcp>=1.0.0` acepto 2.0.0.
Ahora todas van fijadas a la version exacta que corre sana en produccion,
capturada con pip freeze del contenedor healthy.

El lado Next.js ya era reproducible via package-lock.json; por eso el web nunca
se cayo durante el incidente y el api si.

Docs actualizados: AGENTS.md, README.md, api/COTIZADOR_API_SKILL.md y el spec,
que ademas registra en su seccion 0 las tres desviaciones de Fase 0 respecto a
lo disenado (el Despliegue B cancelado, la retirada del MCP y la deriva de
dependencia).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
urieljareth
2026-07-28 17:51:21 -06:00
co-authored by Claude Opus 5
parent edb500f517
commit 20f535d485
9 changed files with 118 additions and 862 deletions
View File
-464
View File
@@ -1,464 +0,0 @@
"""MCP Server for the Cotizador E3 API.
Provides MCP tools and resources for AI agents (OpenClaw, Claude, ChatGPT, etc.)
to interact with the quotation system.
"""
from __future__ import annotations
import json
from datetime import datetime
from mcp.server import Server
from mcp.types import Resource, TextContent, Tool
from app.database import get_pool
from app.mcp.tools import RESOURCES, TOOLS
from app.services.calculators import (
BONOS,
PLANES_BUCEFALO,
calcular_financiamiento,
bucefalo_precio,
)
server = Server("cotizador-e3")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name=t["name"],
description=t["description"],
inputSchema=t["inputSchema"],
)
for t in TOOLS
]
@server.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri=r["uri"],
name=r["name"],
description=r["description"],
mimeType=r["mimeType"],
)
for r in RESOURCES
]
@server.read_resource()
async def read_resource(uri: str) -> str:
pool = await get_pool()
async with pool.acquire() as conn:
if uri == "cotizador://servicios":
rows = await conn.fetch(
"""SELECT s.id, s.nombre, s.descripcion, s.fase, s."tipoPago",
s."precioBase", s."tiempoEntrega", s."entregablesDefault",
s.variante, s.activo, s.orden,
c.nombre as categoria_nombre
FROM "ServicioCatalogo" s
LEFT JOIN "Categoria" c ON s."categoriaId" = c.id
WHERE s.activo = true
ORDER BY s.fase ASC, s.orden ASC"""
)
return json.dumps([dict(r) for r in rows], default=str)
elif uri == "cotizador://categorias":
rows = await conn.fetch(
'SELECT id, nombre, descripcion, color, activo, orden FROM "Categoria" ORDER BY orden ASC'
)
return json.dumps([dict(r) for r in rows], default=str)
elif uri == "cotizador://configuracion":
rows = await conn.fetch('SELECT clave, valor FROM "Configuracion"')
return json.dumps({r["clave"]: r["valor"] for r in rows})
return json.dumps({"error": f"Unknown resource: {uri}"})
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
pool = await get_pool()
async with pool.acquire() as conn:
result = await _handle_tool(conn, name, arguments)
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
async def _handle_tool(conn, name: str, arguments: dict) -> dict:
if name == "buscar_servicios":
return await _buscar_servicios(conn, arguments)
elif name == "crear_cotizacion":
return await _crear_cotizacion(conn, arguments)
elif name == "obtener_cotizacion":
return await _obtener_cotizacion(conn, arguments)
elif name == "listar_cotizaciones":
return await _listar_cotizaciones(conn, arguments)
elif name == "cambiar_estado_cotizacion":
return await _cambiar_estado(conn, arguments)
elif name == "actualizar_precio_servicio":
return await _actualizar_precio(conn, arguments)
elif name == "duplicar_cotizacion":
return await _duplicar_cotizacion(conn, arguments)
elif name == "calcular_financiamiento":
return await _calcular_financiamiento(arguments)
elif name == "generar_pdf_cotizacion":
return await _generar_pdf(conn, arguments)
elif name == "obtener_configuracion":
return await _obtener_configuracion(conn)
elif name == "listar_bonos":
return {"bonos": BONOS}
elif name == "listar_planes_bucefalo":
return {"planes": PLANES_BUCEFALO}
else:
return {"error": f"Unknown tool: {name}"}
async def _buscar_servicios(conn, args: dict) -> dict:
conditions = ['s.activo = true']
params = []
idx = 1
if args.get("fase") is not None:
conditions.append(f's.fase = ${idx}')
params.append(args["fase"])
idx += 1
if args.get("tipo_pago"):
conditions.append(f's."tipoPago" = ${idx}')
params.append(args["tipo_pago"])
idx += 1
if args.get("categoria"):
conditions.append(f'LOWER(c.nombre) = LOWER(${idx})')
params.append(args["categoria"])
idx += 1
if args.get("busqueda"):
conditions.append(f'(LOWER(s.nombre) LIKE LOWER(${idx}) OR LOWER(s.descripcion) LIKE LOWER(${idx}))')
params.append(f"%{args['busqueda']}%")
idx += 1
where = " AND ".join(conditions)
query = f"""SELECT s.id, s.nombre, s.descripcion, s.fase, s."tipoPago",
s."precioBase", s."tiempoEntrega", s."entregablesDefault",
s.variante, c.nombre as categoria
FROM "ServicioCatalogo" s
LEFT JOIN "Categoria" c ON s."categoriaId" = c.id
WHERE {where}
ORDER BY s.fase ASC, s.orden ASC"""
rows = await conn.fetch(query, *params)
servicios = []
for r in rows:
d = dict(r)
if d.get("entregablesDefault") and isinstance(d["entregablesDefault"], str):
try:
d["entregablesDefault"] = json.loads(d["entregablesDefault"])
except (json.JSONDecodeError, TypeError):
pass
servicios.append(d)
return {"servicios": servicios, "total": len(servicios)}
async def _crear_cotizacion(conn, args: dict) -> dict:
cliente_data = args["cliente"]
servicios_data = args["servicios"]
plan_bucefalo = args.get("plan_bucefalo")
moneda = args.get("moneda", "MXN")
proyecto = args.get("proyecto", "MKT Digital")
esquema = args.get("esquema_pago", "Pago Unico/Mensual")
es_doble = bool(args.get("es_doble", False))
opciones_metadata = args.get("opciones_metadata") if es_doble else None
cliente = await conn.fetchrow(
'SELECT id FROM "Cliente" WHERE nombre = $1 AND empresa = $2',
cliente_data["nombre"],
cliente_data.get("empresa", ""),
)
if not cliente:
cliente = await conn.fetchrow(
'INSERT INTO "Cliente" (id, nombre, empresa, email, telefono, "createdAt", "updatedAt") VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW()) RETURNING id',
cliente_data["nombre"],
cliente_data.get("empresa", ""),
cliente_data.get("email", ""),
cliente_data.get("telefono", ""),
)
cliente_id = cliente["id"]
now = datetime.now()
numero = f"UJ{str(now.year)[-2:]}{now.month:02d}AGENT001"
from app.services.calculators import calcular_vigencia
vigencia = calcular_vigencia(now)
async with conn.transaction():
cot = await conn.fetchrow(
"""INSERT INTO "Cotizacion" (id, numero, fecha, vigencia, moneda, "tipoCambio", proyecto, "esquemaPago",
estado, "incluirBonos", "incluirFinanciamiento", "esDoble", "opcionesMetadata", observaciones, "clienteId", "asesorId", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2, $3, $4, 'NA', $5, $6, 'borrador', false, false, $7, $8, '', $9, $10, NOW(), NOW())
RETURNING id, numero""",
numero, now, vigencia, moneda, proyecto, esquema,
es_doble,
json.dumps(opciones_metadata) if opciones_metadata else None,
cliente_id, "agent",
)
cot_id = cot["id"]
for srv in servicios_data:
catalogo_id = srv["servicio_id"]
cat_row = await conn.fetchrow(
'SELECT id, "precioBase", "tiempoEntrega", "entregablesDefault", fase, "tipoPago" FROM "ServicioCatalogo" WHERE id = $1',
catalogo_id,
)
if not cat_row:
continue
precio = srv.get("precio_personalizado") or cat_row["precioBase"]
entregables = cat_row["entregablesDefault"]
if isinstance(entregables, str):
try:
entregables = json.loads(entregables)
except (json.JSONDecodeError, TypeError):
entregables = []
opcion = (srv.get("opcion") or "ambas") if es_doble else None
await conn.fetchrow(
"""INSERT INTO "ServicioCotizado" (id, "cotizacionId", "servicioCatalogoId", fase, "tipoPago",
precio, "tiempoEntrega", entregables, opcion, seleccionado, "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, true, NOW(), NOW())""",
cot_id, catalogo_id, cat_row["fase"], cat_row["tipoPago"],
precio, cat_row["tiempoEntrega"], json.dumps(entregables or []), opcion,
)
if plan_bucefalo:
nivel = plan_bucefalo if isinstance(plan_bucefalo, str) else plan_bucefalo.get("nivel", "basico")
precio_bp = bucefalo_precio(nivel)
await conn.fetchrow(
"""INSERT INTO "PlanBucefaloCotizacion" (id, "cotizacionId", nivel, precio, seleccionado, "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2, $3, true, NOW(), NOW())""",
cot_id, nivel, precio_bp,
)
return {"cotizacion_id": str(cot_id), "numero": numero, "estado": "borrador", "cliente_id": str(cliente_id)}
async def _obtener_cotizacion(conn, args: dict) -> dict:
# Lista blanca de columnas: esta herramienta devuelve la fila completa al agente
# (result = dict(cot) mas abajo), asi que un SELECT c.* publicaria cualquier
# columna nueva sin que nadie lo decida. Espeja CotizacionResponse del REST
# (app/models/cotizacion.py) y excluye deliberadamente "observacionesInternas",
# que es texto que no debe salir de la app.
cot = await conn.fetchrow(
"""SELECT c.id, c.numero, c.fecha, c.vigencia, c.moneda, c."tipoCambio",
c.proyecto, c."esquemaPago", c.estado, c."incluirBonos",
c."incluirFinanciamiento", c."incluirIva", c."esDoble",
c."opcionesMetadata", c.observaciones, c."clienteId", c."asesorId",
c."createdAt", c."updatedAt",
cl.nombre as cliente_nombre, cl.empresa as cliente_empresa,
cl.email as cliente_email, cl.telefono as cliente_telefono
FROM "Cotizacion" c
LEFT JOIN "Cliente" cl ON c."clienteId" = cl.id
WHERE c.id = $1""",
args["cotizacion_id"],
)
if not cot:
return {"error": "Cotización no encontrada"}
servicios = await conn.fetch(
"""SELECT sc.*, s.nombre as servicio_nombre, s.fase as servicio_fase, s."tipoPago" as "servicio_tipoPago"
FROM "ServicioCotizado" sc
LEFT JOIN "ServicioCatalogo" s ON sc."servicioCatalogoId" = s.id
WHERE sc."cotizacionId" = $1""",
args["cotizacion_id"],
)
plan = await conn.fetchrow(
"""SELECT id, "cotizacionId", nivel, precio, seleccionado, "createdAt", "updatedAt"
FROM "PlanBucefaloCotizacion" WHERE "cotizacionId" = $1""",
args["cotizacion_id"],
)
result = dict(cot)
result["cliente"] = {
"nombre": cot["cliente_nombre"],
"empresa": cot["cliente_empresa"],
"email": cot["cliente_email"],
"telefono": cot["cliente_telefono"],
}
result["servicios"] = [dict(s) for s in servicios]
result["planBucefalo"] = dict(plan) if plan else None
for k in ["cliente_nombre", "cliente_empresa", "cliente_email", "cliente_telefono"]:
result.pop(k, None)
return result
async def _listar_cotizaciones(conn, args: dict) -> dict:
conditions = []
params = []
idx = 1
if args.get("estado"):
conditions.append(f'c.estado = ${idx}')
params.append(args["estado"])
idx += 1
if args.get("cliente_nombre"):
conditions.append(f'LOWER(cl.nombre) LIKE LOWER(${idx})')
params.append(f"%{args['cliente_nombre']}%")
idx += 1
if args.get("busqueda"):
conditions.append(
f'(LOWER(c.numero) LIKE LOWER(${idx}) OR LOWER(c.proyecto) LIKE LOWER(${idx}) OR LOWER(cl.nombre) LIKE LOWER(${idx}))'
)
params.append(f"%{args['busqueda']}%")
idx += 1
where = "WHERE " + " AND ".join(conditions) if conditions else ""
query = f"""SELECT c.id, c.numero, c.fecha, c.vigencia, c.estado, c.proyecto,
c.moneda, c."esquemaPago",
cl.nombre as cliente_nombre, cl.empresa as cliente_empresa
FROM "Cotizacion" c
LEFT JOIN "Cliente" cl ON c."clienteId" = cl.id
{where}
ORDER BY c."createdAt" DESC
LIMIT 50"""
rows = await conn.fetch(query, *params)
cotizaciones = []
for r in rows:
d = dict(r)
d["cliente"] = {"nombre": d.pop("cliente_nombre"), "empresa": d.pop("cliente_empresa")}
cotizaciones.append(d)
return {"cotizaciones": cotizaciones, "total": len(cotizaciones)}
async def _cambiar_estado(conn, args: dict) -> dict:
cot_id = args["cotizacion_id"]
estado = args["estado"]
cot = await conn.fetchrow('SELECT id FROM "Cotizacion" WHERE id = $1', cot_id)
if not cot:
return {"error": "Cotización no encontrada"}
await conn.execute('UPDATE "Cotizacion" SET estado = $1, "updatedAt" = NOW() WHERE id = $2', estado, cot_id)
return {"ok": True, "cotizacion_id": cot_id, "nuevo_estado": estado}
async def _actualizar_precio(conn, args: dict) -> dict:
cot_id = args["cotizacion_id"]
servicio_id = args["servicio_id"]
nuevo_precio = args["nuevo_precio"]
srv = await conn.fetchrow(
'SELECT id FROM "ServicioCotizado" WHERE id = $1 AND "cotizacionId" = $2',
servicio_id, cot_id,
)
if not srv:
return {"error": "Servicio no encontrado en esta cotización"}
await conn.execute(
'UPDATE "ServicioCotizado" SET precio = $1, "updatedAt" = NOW() WHERE id = $2',
nuevo_precio, servicio_id,
)
return {"ok": True, "servicio_id": servicio_id, "nuevo_precio": nuevo_precio}
async def _duplicar_cotizacion(conn, args: dict) -> dict:
cot_id = args["cotizacion_id"]
original = await conn.fetchrow('SELECT * FROM "Cotizacion" WHERE id = $1', cot_id)
if not original:
return {"error": "Cotización no encontrada"}
now = datetime.now()
new_numero = f"{original['numero']}-COPY"
async with conn.transaction():
new_cot = await conn.fetchrow(
"""INSERT INTO "Cotizacion" (id, numero, fecha, vigencia, moneda, "tipoCambio", proyecto, "esquemaPago",
estado, "incluirBonos", "incluirFinanciamiento", "esDoble", "opcionesMetadata", observaciones, "clienteId", "asesorId", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, 'borrador', $8, $9, $10, $11, $12, $13, $14, NOW(), NOW())
RETURNING id, numero""",
new_numero, now, original["vigencia"], original["moneda"], original["tipoCambio"],
original["proyecto"], original["esquemaPago"], original["incluirBonos"],
original["incluirFinanciamiento"], original["esDoble"], original["opcionesMetadata"],
original["observaciones"], original["clienteId"], original["asesorId"],
)
new_id = new_cot["id"]
servicios = await conn.fetch(
'SELECT * FROM "ServicioCotizado" WHERE "cotizacionId" = $1', cot_id
)
for s in servicios:
await conn.fetchrow(
"""INSERT INTO "ServicioCotizado" (id, "cotizacionId", "servicioCatalogoId", fase, "tipoPago",
precio, "tiempoEntrega", entregables, notas, opcion, seleccionado, "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())""",
new_id, s["servicioCatalogoId"], s["fase"], s["tipoPago"],
s["precio"], s["tiempoEntrega"], s["entregables"], s["notas"], s["opcion"], s["seleccionado"],
)
plan = await conn.fetchrow(
'SELECT * FROM "PlanBucefaloCotizacion" WHERE "cotizacionId" = $1', cot_id
)
if plan:
await conn.fetchrow(
"""INSERT INTO "PlanBucefaloCotizacion" (id, "cotizacionId", nivel, precio, seleccionado, "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW(), NOW())""",
new_id, plan["nivel"], plan["precio"], plan["seleccionado"],
)
return {"cotizacion_id": str(new_id), "numero": new_numero, "estado": "borrador"}
async def _calcular_financiamiento(args: dict) -> dict:
monto = args["monto"]
meses = args["meses"]
plan = next((p for p in FINANCIAMIENTO_PLANES if p["meses"] == meses), None)
if not plan:
from app.services.calculators import FINANCIAMIENTO_PLANES
plan = next((p for p in FINANCIAMIENTO_PLANES if p["meses"] == meses), None)
if not plan:
return {"error": f"Plan de {meses} meses no disponible"}
result = calcular_financiamiento(monto, meses, plan["tasa"], plan["comision"])
result["meses"] = meses
result["tasa"] = plan["tasa"]
result["comision"] = plan["comision"]
return result
async def _generar_pdf(conn, args: dict) -> dict:
cot_id = args["cotizacion_id"]
cot = await conn.fetchrow(
"""SELECT c.*, cl.nombre as cliente_nombre, cl.empresa as cliente_empresa
FROM "Cotizacion" c
LEFT JOIN "Cliente" cl ON c."clienteId" = cl.id
WHERE c.id = $1""",
cot_id,
)
if not cot:
return {"error": "Cotización no encontrada"}
return {
"status": "pdf_generated",
"cotizacion_id": cot_id,
"numero": cot["numero"],
"filename": f"{cot['cliente_nombre']} - {cot['numero']}.pdf",
"message": "PDF generation will be implemented with reportlab",
}
async def _obtener_configuracion(conn) -> dict:
rows = await conn.fetch('SELECT clave, valor FROM "Configuracion"')
return {"config": {r["clave"]: r["valor"] for r in rows}}
from app.services.calculators import FINANCIAMIENTO_PLANES
-305
View File
@@ -1,305 +0,0 @@
"""MCP (Model Context Protocol) tool definitions for the Cotizador API.
Each tool is designed to be semantically clear for AI agents like OpenClaw, Claude, and ChatGPT.
"""
TOOLS = [
{
"name": "buscar_servicios",
"description": (
"Busca servicios del catálogo de marketing digital de Consultoría E3. "
"Útil cuando el cliente pregunta por servicios disponibles, precios, o por fase del proyecto. "
"Las fases son: 0=Auditoría (diagnóstico inicial), 1=Setup (infraestructura y configuración), "
"2=Publicidad (anuncios y manejo de redes), 3=Contenido/SEO (producción de contenido y posicionamiento). "
"Tipos de pago: 'unico' (pago único) o 'mensual' (recurso recurrente)."
),
"inputSchema": {
"type": "object",
"properties": {
"fase": {
"type": "integer",
"enum": [0, 1, 2, 3],
"description": "Fase del proyecto: 0=Auditoría/Acompañamiento, 1=Setup/Infraestructura, 2=Publicidad/Manejo, 3=Contenido/SEO",
},
"tipo_pago": {
"type": "string",
"enum": ["unico", "mensual"],
"description": "Tipo de pago: 'unico' para pago único, 'mensual' para recurrente",
},
"categoria": {
"type": "string",
"description": "Nombre de categoría: SEO, Marketing, Paid Media, Desarrollo Web, Automatizaciones, CRM, Desarrollo Personalizado",
},
"busqueda": {
"type": "string",
"description": "Texto libre para buscar en nombre y descripción del servicio",
},
},
},
},
{
"name": "crear_cotizacion",
"description": (
"Crea una cotización completa de servicios de marketing digital para un cliente. "
"El cliente se crea automáticamente si no existe (busca por nombre+empresa). "
"Incluye servicios del catálogo con precios personalizables, plan CRM Bucefalo opcional, "
"y configuración de moneda y esquema de pago. "
"La cotización se crea en estado 'borrador'. "
"Precios CRM Bucefalo: basico=$1,000/mes, estandar=$3,500/mes, premium=$4,500/mes, empresarial=$7,500/mes. "
"Soporta DOBLE PROPUESTA: con es_doble=true se presentan dos opciones comparables; cada servicio "
"se asigna a la opción '1', '2' o 'ambas' (compartido), y opciones_metadata define el título, "
"descripción y exclusiones de cada opción."
),
"inputSchema": {
"type": "object",
"properties": {
"cliente": {
"type": "object",
"properties": {
"nombre": {"type": "string", "description": "Nombre completo del contacto"},
"empresa": {"type": "string", "description": "Nombre de la empresa (opcional)"},
"email": {"type": "string", "description": "Email de contacto"},
"telefono": {"type": "string", "description": "Teléfono de contacto"},
},
"required": ["nombre"],
},
"servicios": {
"type": "array",
"items": {
"type": "object",
"properties": {
"servicio_id": {"type": "string", "description": "ID del servicio del catálogo (obtener con buscar_servicios)"},
"precio_personalizado": {"type": "number", "description": "Precio personalizado (opcional, usa precio base si no se especifica)"},
"opcion": {"type": "string", "enum": ["1", "2", "ambas"], "description": "Solo en doble propuesta: opción a la que pertenece el servicio ('ambas' = compartido). Default 'ambas'."},
},
"required": ["servicio_id"],
},
"description": "Lista de servicios a incluir en la cotización",
},
"es_doble": {
"type": "boolean",
"description": "Si es true, la cotización presenta dos opciones comparables (doble propuesta).",
},
"opciones_metadata": {
"type": "object",
"description": "Solo en doble propuesta. Metadatos por opción, p.ej. {\"1\": {\"titulo\": \"...\", \"descripcion\": \"...\", \"noIncluye\": \"...\"}, \"2\": {...}}.",
"properties": {
"1": {"type": "object", "properties": {"titulo": {"type": "string"}, "descripcion": {"type": "string"}, "noIncluye": {"type": "string"}}},
"2": {"type": "object", "properties": {"titulo": {"type": "string"}, "descripcion": {"type": "string"}, "noIncluye": {"type": "string"}}},
},
},
"plan_bucefalo": {
"type": "string",
"enum": ["basico", "estandar", "premium", "empresarial"],
"description": "Nivel del plan CRM Bucefalo (opcional)",
},
"proyecto": {
"type": "string",
"description": "Nombre o descripción del proyecto (default: 'MKT Digital')",
},
"moneda": {
"type": "string",
"enum": ["MXN", "USD"],
"description": "Moneda de la cotización (default: MXN)",
},
"esquema_pago": {
"type": "string",
"enum": ["Pago Unico", "Mensual", "Pago Unico/Mensual"],
"description": "Esquema de pago (default: Pago Unico/Mensual)",
},
},
"required": ["cliente", "servicios"],
},
},
{
"name": "obtener_cotizacion",
"description": (
"Obtiene los detalles completos de una cotización existente incluyendo: "
"datos del cliente, servicios seleccionados con precios, estado actual, "
"plan CRM Bucefalo si aplica, observaciones, fechas y vigencia."
),
"inputSchema": {
"type": "object",
"properties": {
"cotizacion_id": {"type": "string", "description": "ID de la cotización"},
},
"required": ["cotizacion_id"],
},
},
{
"name": "listar_cotizaciones",
"description": (
"Lista cotizaciones con filtros opcionales. "
"Útil para revisar el pipeline de ventas, cotizaciones pendientes, o historial de un cliente. "
"Estados: borrador (en proceso), enviada (esperando respuesta), aprobada (cerrada ganada), rechazada (cerrada perdida)."
),
"inputSchema": {
"type": "object",
"properties": {
"estado": {
"type": "string",
"enum": ["borrador", "enviada", "aprobada", "rechazada"],
"description": "Filtrar por estado",
},
"cliente_nombre": {
"type": "string",
"description": "Buscar por nombre de cliente",
},
"busqueda": {
"type": "string",
"description": "Texto libre para buscar en número, proyecto o cliente",
},
},
},
},
{
"name": "cambiar_estado_cotizacion",
"description": (
"Cambia el estado de una cotización. "
"Flujo normal: borrador → enviada → aprobada o rechazada. "
"Solo cambiar a 'enviada' cuando la cotización esté lista para el cliente. "
"Cambiar a 'aprobada' cuando el cliente acepte, o 'rechazada' cuando decline."
),
"inputSchema": {
"type": "object",
"properties": {
"cotizacion_id": {"type": "string", "description": "ID de la cotización"},
"estado": {
"type": "string",
"enum": ["borrador", "enviada", "aprobada", "rechazada"],
"description": "Nuevo estado de la cotización",
},
},
"required": ["cotizacion_id", "estado"],
},
},
{
"name": "actualizar_precio_servicio",
"description": (
"Actualiza el precio de un servicio específico dentro de una cotización. "
"No modifica el precio base del catálogo, solo el precio en esta cotización. "
"Útil para negociar precios individuales sin recrear toda la cotización."
),
"inputSchema": {
"type": "object",
"properties": {
"cotizacion_id": {"type": "string", "description": "ID de la cotización"},
"servicio_id": {"type": "string", "description": "ID del servicio cotizado (no el del catálogo)"},
"nuevo_precio": {"type": "number", "description": "Nuevo precio en la moneda de la cotización"},
},
"required": ["cotizacion_id", "servicio_id", "nuevo_precio"],
},
},
{
"name": "duplicar_cotizacion",
"description": (
"Duplica una cotización existente como nueva copia en estado 'borrador'. "
"Crea una copia exacta con nuevo ID y número. "
"Útil para crear variaciones de una propuesta o reenviar una cotización actualizada."
),
"inputSchema": {
"type": "object",
"properties": {
"cotizacion_id": {"type": "string", "description": "ID de la cotización a duplicar"},
},
"required": ["cotizacion_id"],
},
},
{
"name": "calcular_financiamiento",
"description": (
"Calcula las mensualidades para financiar una cotización o monto específico. "
"Plazos disponibles: 3 meses (7.7% tasa), 6 meses (10.7%), 9 meses (13.7%), 12 meses (16.7%). "
"Todos incluyen 2.5% de comisión + 16% IVA. "
"Devuelve: pago mensual, IVA mensual, total mensual, comisión total, y gran total."
),
"inputSchema": {
"type": "object",
"properties": {
"monto": {"type": "number", "description": "Monto total a financiar en MXN"},
"meses": {"type": "integer", "enum": [3, 6, 9, 12], "description": "Plazo en meses"},
},
"required": ["monto", "meses"],
},
},
{
"name": "generar_pdf_cotizacion",
"description": (
"Genera un PDF profesional de una cotización con: logo de la empresa, colores de marca, "
"tabla de servicios agrupados por fase, bonos incluidos, términos y condiciones, "
"y datos bancarios para transferencia. Devuelve el archivo PDF."
),
"inputSchema": {
"type": "object",
"properties": {
"cotizacion_id": {"type": "string", "description": "ID de la cotización guardada"},
},
"required": ["cotizacion_id"],
},
},
{
"name": "obtener_configuracion",
"description": (
"Obtiene la configuración de la empresa Consultoría E3: "
"razón social, RFC, domicilio fiscal, datos bancarios (cuenta nacional, CLABE, cuenta internacional, SWIFT), "
"colores de marca, logo, y términos y condiciones. "
"Útil para generar documentos o verificar información fiscal."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "listar_bonos",
"description": (
"Lista los bonos/disponibles que se pueden incluir en una cotización: "
"1) Servicio Centinela Web (monitoreo 30 min/mes), "
"2) Workshop Buyer Persona, "
"3) Workshop Propuesta de Valor, "
"4) Membresía Premium (1 año), "
"5) Mes Gratis CRM Bucefalo, "
"6) Script de Ventas (100+ complementos)."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "listar_planes_bucefalo",
"description": (
"Lista los niveles del CRM Bucefalo con precios mensuales: "
"Básico ($1,000/mes), Estándar ($3,500/mes), Premium ($4,500/mes), Empresarial ($7,500/mes). "
"Bucefalo es un CRM para gestión de ventas y clientes."
),
"inputSchema": {"type": "object", "properties": {}},
},
]
RESOURCES = [
{
"uri": "cotizador://servicios",
"name": "Catálogo de Servicios",
"description": (
"Lista completa de servicios de marketing digital de Consultoría E3 organizados por fase: "
"Fase 0 (Auditorías), Fase 1 (Setup/Infraestructura), Fase 2 (Publicidad/Manejo), "
"Fase 3 (Contenido/SEO). Cada servicio incluye nombre, descripción, precio base, "
"tiempo de entrega, tipo de pago (único/mensual), y entregables."
),
"mimeType": "application/json",
},
{
"uri": "cotizador://categorias",
"name": "Categorías de Servicios",
"description": (
"Categorías disponibles para clasificar servicios: "
"SEO, Marketing, Paid Media, Desarrollo Web, Automatizaciones, CRM, Desarrollo Personalizado."
),
"mimeType": "application/json",
},
{
"uri": "cotizador://configuracion",
"name": "Configuración de la Empresa",
"description": (
"Datos fiscales, bancarios y de marca de Consultoría E3. "
"Incluye razón social, RFC, domicilio fiscal, cuentas bancarias nacionales e internacionales, "
"colores de marca, logo, y términos y condiciones."
),
"mimeType": "application/json",
},
]