Devuelve entity_id, user_id y person_id de la sesión OAuth.
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.Recibir mensaje WhatsApp y extraer phone (E.164, ej. +569…).
- 2.Si no hay JWT de integración o expiró → generateToken (header id + customer_id/email/password).
- 3.POST resolveByPhone con Bearer JWT.
- 4.Si access_token es null → responder que no hay cuenta empresa asociada; no llamar MCP.
- 5.Con access_token → POST MCP: initialize (opcional), tools/list, tools/call.
- 6.Si MCP responde 401 → volver a resolveByPhone y reintentar una vez.
curl · flujo completo
# 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.
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
// 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
Ingesta PDF/DOCX en base64 (adjuntos del chat). Devuelve document_id. No acepta URL.
RAG
Busca contenido en documentos ingeridos (búsqueda semántica).
Extrae firmantes del PDF y aplica tipo de documento (manual o auto-detectado por título).
Firma
Envía documento a firma desde el agente (checkout + firmantes).
Operaciones
Detalle de proceso ligado a una operación (firmantes, IDs, links).
Busca operaciones por RUT de firmante en un rango de fechas.
Busca operaciones por teléfono de firmante en un rango de fechas.
Gestiona el flujo de firma: envío, reenvío, datos, tipo de firma, links, turno y eliminación.
Firma masiva
Firma masiva CDS: listar pendientes, pedir código y firmar.
Wallet
Consulta o recarga saldo de wallet del partner autenticado.
Templates
Busca plantillas, devuelve grupos de campos a completar y envía a firmar (PDF + wallet).
axios · tools/list y tools/call
// 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 }, ...]// 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.
// 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.