PortalFirma

MCP · WhatsApp / N8N

Flujo por teléfono

No usa el browser OAuth de Claude. Mintea el Bearer MCP vía api-integration (resolveByPhone → api interno) tras identificar al partner por phone.

Cadena: generateToken resolveByPhone POST MCP con Bearer pf_mcp_at_*.

Si el teléfono no pertenece a un partner, access_token viene null. Si el token MCP expira (~1h), vuelve a llamar resolveByPhone. La respuesta no incluye entity_id / user_id / person_id: van cifrados en el Bearer.

Patrón N8N

  1. 1.Recibir mensaje WhatsApp y extraer phone (E.164, ej. +569…).
  2. 2.Si no hay JWT de integración o expiró → generateToken (header id + customer_id/email/password).
  3. 3.POST resolveByPhone con Bearer JWT.
  4. 4.Si access_token es null → responder que no hay cuenta empresa asociada; no llamar MCP.
  5. 5.Con access_token → POST MCP: initialize (opcional), tools/list, tools/call.
  6. 6.Si MCP responde 401 → volver a resolveByPhone y reintentar una vez.

curl · flujo completo

curl
# 1) JWT integración
curl -X POST https://integracion.portalfirma.cl/api/auth/generateToken \
  -H "Content-Type: application/json" \
  -H "id: <API_KEY>" \
  -d '{"customer_id":"<uuid>","email":"...","password":"..."}'

# 2) Sesión + access_token MCP
curl -X POST https://integracion.portalfirma.cl/api/session/resolveByPhone \
  -H "Content-Type: application/json" \
  -H "id: <API_KEY>" \
  -H "Authorization: Bearer <jwt>" \
  -d '{"phone":"+56912345678"}'

# 3) MCP
curl -X POST https://mcp.portalfirma.cl/api/mcp \
  -H "Authorization: Bearer <pf_mcp_at_...>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

axios · integración completa

Mismo flujo para un Code node de N8N o un bot Node.js. Headers obligatorios en MCP: Authorization, Content-Type, Accept.

axios · generateToken → MCP
import axios from "axios";

const INTEGRATION = "https://integracion.portalfirma.cl";
const MCP_URL = "https://mcp.portalfirma.cl/api/mcp";
const API_KEY = process.env.PORTAL_API_KEY; // header "id"

const http = axios.create({
  headers: {
    "Content-Type": "application/json",
    id: API_KEY,
  },
});

/** 1) JWT de integración (cachear hasta que expire) */
async function generateIntegrationJwt({
  customerId,
  email,
  password,
}) {
  const { data } = await http.post(`${INTEGRATION}/api/auth/generateToken`, {
    customer_id: customerId,
    email,
    password,
  });
  return data.token; // eyJ...
}

/** 2) Mint access_token MCP por teléfono */
async function resolveByPhone(jwt, phone) {
  const { data } = await http.post(
    `${INTEGRATION}/api/session/resolveByPhone`,
    { phone },
    { headers: { Authorization: `Bearer ${jwt}` } }
  );
  // data.data = { access_token, token_type, expires_in } | access_token: null
  return data.data;
}

/** 3) Llamada JSON-RPC al resource MCP */
async function mcpCall(accessToken, method, params = {}, id = 1) {
  const { data } = await axios.post(
    MCP_URL,
    { jsonrpc: "2.0", id, method, params },
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
        Accept: "application/json, text/event-stream",
      },
    }
  );
  return data;
}

/** Flujo típico N8N / WhatsApp por mensaje */
async function handleWhatsAppMessage({ phone, customerId, email, password }) {
  const jwt = await generateIntegrationJwt({ customerId, email, password });
  const session = await resolveByPhone(jwt, phone);

  if (!session?.access_token) {
    // Teléfono no es partnerUser → no llamar MCP
    return { ok: false, reason: "no_partner_for_phone" };
  }

  const listed = await mcpCall(session.access_token, "tools/list", {});
  const sessionCtx = await mcpCall(
    session.access_token,
    "tools/call",
    { name: "get_partner_session", arguments: {} },
    2
  );

  return { ok: true, listed, sessionCtx, expiresIn: session.expires_in };
}

resolveByPhone · respuestas

JSON · data
// Partner encontrado
{
  "success": true,
  "data": {
    "access_token": "pf_mcp_at_...",
    "token_type": "Bearer",
    "expires_in": 3600
  },
  "error": null
}

// Teléfono sin partner → no llamar MCP
{
  "success": true,
  "data": {
    "access_token": null,
    "token_type": null,
    "expires_in": null
  },
  "error": null
}

Tras el Bearer, tools/list devuelve el catálogo vivo. Resumen de tools registradas:

Session

Document

ingest_document_tools

Ingesta PDF/DOCX en base64 (adjuntos del chat). Devuelve document_id. No acepta URL.

RAG

PDF

Firma

Operaciones

Firma masiva

Wallet

Templates

template_manage_tools

Busca plantillas, devuelve grupos de campos a completar y envía a firmar (PDF + wallet).

axios · tools/list y tools/call

axios · tools/list
// Listar tools disponibles (requiere Bearer)
const { data } = await axios.post(
  "https://mcp.portalfirma.cl/api/mcp",
  {
    jsonrpc: "2.0",
    id: 1,
    method: "tools/list",
    params: {},
  },
  {
    headers: {
      Authorization: "Bearer pf_mcp_at_...",
      "Content-Type": "application/json",
      Accept: "application/json, text/event-stream",
    },
  }
);

// data.result.tools → [{ name, description, inputSchema }, ...]
axios · tools/call
// Invocar una tool
const { data } = await axios.post(
  "https://mcp.portalfirma.cl/api/mcp",
  {
    jsonrpc: "2.0",
    id: 2,
    method: "tools/call",
    params: {
      name: "get_partner_session",
      arguments: {},
    },
  },
  {
    headers: {
      Authorization: "Bearer pf_mcp_at_...",
      "Content-Type": "application/json",
      Accept: "application/json, text/event-stream",
    },
  }
);

MCP sin token ¿qué pasa?

Sin header Authorization

HTTP 401 + header WWW-Authenticate con resource_metadata hacia el discovery OAuth. Body JSON-RPC con code -32001 y mensaje «Bearer OAuth requerido».

Token inválido / expirado / revocado

También 401. En WhatsApp/N8N no hay refresh_token: vuelve a llamar resolveByPhone y reemplaza el Bearer.

Ya no hay modo anónimo

No existen access_mode, has_active_mcp_token ni tools públicas sin Bearer. Sin token no se listan ni se ejecutan tools.

Discovery resource: https://mcp.portalfirma.cl/.well-known/oauth-protected-resource. En este flujo WhatsApp/N8N no sigues el browser OAuth: obtienes el Bearer solo con resolveByPhone.

axios · sin Bearer
// Sin Authorization → 401 (no hay tools anónimas)
try {
  await axios.post(
    "https://mcp.portalfirma.cl/api/mcp",
    {
      jsonrpc: "2.0",
      id: 1,
      method: "tools/list",
      params: {},
    },
    {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json, text/event-stream",
      },
      validateStatus: () => true, // inspeccionar 401
    }
  );
} catch (err) {
  // axios lanza si validateStatus por defecto
}

/*
Respuesta típica:
HTTP 401
WWW-Authenticate: Bearer ... resource_metadata="https://mcp.portalfirma.cl/.well-known/oauth-protected-resource"

Body:
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Bearer OAuth requerido"
  },
  "id": null
}
*/

Renovación

El access_token dura ~1h (expires_in). Opciones: llamar resolveByPhone en cada mensaje, o cachear por teléfono y renovar ~5 min antes. No hay refresh_token en este mint first-party.

Nota

Ya no existen access_mode ni has_active_mcp_token. El Bearer OAuth es el único mecanismo.