@azur.com.ec/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/package.json +13 -0
- package/servidor.js +154 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Servidor MCP de AZUR
|
|
2
|
+
|
|
3
|
+
Deja que un asistente de IA (Claude, Cursor, VS Code, n8n, la API de OpenAI o de Anthropic…) **busque clientes y
|
|
4
|
+
productos, consulte existencias, el plan y los comprobantes, y prepare comprobantes** en AZUR, el sistema de facturación
|
|
5
|
+
electrónica del SRI de Ecuador.
|
|
6
|
+
|
|
7
|
+
## 1. Cree su credencial
|
|
8
|
+
|
|
9
|
+
Entre a AZUR con la cuenta del **dueño** → menú de usuario → **Credenciales API** → *Nueva credencial*.
|
|
10
|
+
|
|
11
|
+
- Elija el **punto de emisión**. Si está en pruebas, la credencial empieza con `azur_test_`; si está en producción, con
|
|
12
|
+
`azur_live_` (lo que guarde es real).
|
|
13
|
+
- Elija los **permisos**. Para un asistente recomendamos: *Consultar comprobantes*, *Crear y editar borradores* y *Ver
|
|
14
|
+
clientes y productos*. **No** le dé *Enviar al SRI y anular* salvo que lo necesite de verdad.
|
|
15
|
+
- Opcional: IP autorizadas y fecha de vencimiento.
|
|
16
|
+
- Copie la credencial: **solo se muestra una vez**.
|
|
17
|
+
|
|
18
|
+
## 2a. Opción recomendada: servidor remoto (sin instalar nada)
|
|
19
|
+
|
|
20
|
+
URL del servidor: `https://azur.com.ec/mcp` (transporte *Streamable HTTP*), autenticación con la cabecera
|
|
21
|
+
`Authorization: Bearer azur_live_…`.
|
|
22
|
+
|
|
23
|
+
**Claude Code**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
claude mcp add --transport http azur https://azur.com.ec/mcp --header "Authorization: Bearer azur_live_..."
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Cursor / VS Code / Windsurf** (configuración MCP):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"azur": {
|
|
35
|
+
"url": "https://azur.com.ec/mcp",
|
|
36
|
+
"headers": { "Authorization": "Bearer azur_live_..." }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**API de Anthropic (conector MCP)**: `url: https://azur.com.ec/mcp`, `authorization_token: azur_live_...`.
|
|
43
|
+
**API de OpenAI (herramienta MCP remota)**: `server_url: https://azur.com.ec/mcp`, `headers: { "Authorization": "Bearer azur_live_..." }`.
|
|
44
|
+
|
|
45
|
+
> Las aplicaciones web de Claude.ai y ChatGPT, por ahora, solo agregan conectores con inicio de sesión OAuth; para ellas
|
|
46
|
+
> use la opción local (2b) desde la app de escritorio.
|
|
47
|
+
|
|
48
|
+
## 2b. Opción local (Claude Desktop y otros clientes por *stdio*)
|
|
49
|
+
|
|
50
|
+
Requiere Node.js 18 o superior. En el archivo de configuración del cliente:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"azur": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "@azur.com.ec/mcp"],
|
|
58
|
+
"env": { "AZUR_API_KEY": "azur_live_..." }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Variables: `AZUR_API_KEY` (obligatoria), `AZUR_URL` (por defecto `https://azur.com.ec`) y `AZUR_PERMITIR_ENVIO=1`
|
|
65
|
+
(solo si quiere que el asistente pueda enviar al SRI).
|
|
66
|
+
|
|
67
|
+
## Lo que puede y lo que no
|
|
68
|
+
|
|
69
|
+
Puede buscar clientes, productos y proveedores, ver existencias, bodegas y kardex, consultar el plan, el resumen de ventas
|
|
70
|
+
y los comprobantes, descargar el PDF, crear clientes y **preparar** facturas, notas de venta y proformas como **borrador**.
|
|
71
|
+
|
|
72
|
+
**No envía nada al SRI** por defecto: el asistente prepara y una persona revisa y envía desde AZUR. En el servidor remoto
|
|
73
|
+
el envío solo aparece si la URL lleva `?envio=1`, y aun así la credencial debe tener el permiso *Enviar al SRI y anular*.
|
|
74
|
+
|
|
75
|
+
Todo pasa por la API normal de AZUR con **su** credencial: se respetan sus permisos, sus IP autorizadas, su ambiente y
|
|
76
|
+
sus límites. Si una búsqueda devuelve varios candidatos, el asistente recibe el aviso de que **debe preguntar** cuál es
|
|
77
|
+
antes de facturar.
|
|
78
|
+
|
|
79
|
+
Las herramientas no están escritas a mano: salen de la propia API (`/plataforma/api/v2/herramientas`), así que cuando
|
|
80
|
+
AZUR agrega una, aparece sola.
|
|
81
|
+
|
|
82
|
+
Documentación de la API: https://azur.com.ec/api/referencia
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@azur.com.ec/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Servidor MCP de AZUR: deja que un asistente de IA consulte y prepare comprobantes electrónicos del SRI (Ecuador)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "azur-mcp": "./servidor.js" },
|
|
8
|
+
"files": ["servidor.js", "README.md"],
|
|
9
|
+
"keywords": ["mcp", "model-context-protocol", "azur", "sri", "ecuador", "facturacion-electronica", "factura"],
|
|
10
|
+
"homepage": "https://azur.com.ec/api",
|
|
11
|
+
"dependencies": { "@modelcontextprotocol/sdk": "^1.17.0" },
|
|
12
|
+
"engines": { "node": ">=18" }
|
|
13
|
+
}
|
package/servidor.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SERVIDOR MCP DE AZUR
|
|
4
|
+
*
|
|
5
|
+
* Deja que un asistente busque clientes y productos, mire existencias y **prepare** facturas
|
|
6
|
+
* en AZUR. Las herramientas no están escritas a mano: se leen de `/api/v2/herramientas`, que
|
|
7
|
+
* a su vez sale del OpenAPI. Si mañana se añade un endpoint, aparece aquí sin tocar nada.
|
|
8
|
+
*
|
|
9
|
+
* ── LO QUE NO PUEDE HACER, Y ES A PROPÓSITO ───────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* No puede **enviar** nada al SRI. Prepara el borrador y ahí se para: quien lo revisa y lo
|
|
12
|
+
* manda es una persona. Un comprobante autorizado por equivocación no se borra, hay que
|
|
13
|
+
* anularlo, y eso se le explica al cliente. Si de verdad se quiere abrir esa puerta, se
|
|
14
|
+
* arranca con AZUR_PERMITIR_ENVIO=1, pero que sea una decisión de alguien.
|
|
15
|
+
*
|
|
16
|
+
* Uso:
|
|
17
|
+
* AZUR_URL=https://azur.com.ec AZUR_API_KEY=azur_live_... npx azur-mcp
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import {
|
|
23
|
+
CallToolRequestSchema,
|
|
24
|
+
ListToolsRequestSchema,
|
|
25
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
|
|
27
|
+
const URL_BASE = (process.env.AZUR_URL || "https://azur.com.ec").replace(/\/$/, "");
|
|
28
|
+
const API = `${URL_BASE}/plataforma/api/v2`;
|
|
29
|
+
const CREDENCIAL = process.env.AZUR_API_KEY;
|
|
30
|
+
const PERMITIR_ENVIO = process.env.AZUR_PERMITIR_ENVIO === "1";
|
|
31
|
+
|
|
32
|
+
if (!CREDENCIAL) {
|
|
33
|
+
console.error("Falta AZUR_API_KEY. Debe empezar por azur_live_ o azur_test_.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Un aviso que conviene ver antes de facturarle a alguien de verdad.
|
|
38
|
+
if (CREDENCIAL.startsWith("azur_live_")) {
|
|
39
|
+
console.error("AVISO: credencial de PRODUCCIÓN. Lo que se guarde es real.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cabeceras = {
|
|
43
|
+
"X-Api-Key": CREDENCIAL,
|
|
44
|
+
"Accept": "application/json",
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Las herramientas salen de la propia API, que las genera del OpenAPI. */
|
|
49
|
+
async function cargarHerramientas() {
|
|
50
|
+
const r = await fetch(`${API}/herramientas?envio=${PERMITIR_ENVIO ? 1 : 0}`, {
|
|
51
|
+
headers: cabeceras,
|
|
52
|
+
});
|
|
53
|
+
if (!r.ok) throw new Error(`No se pudo leer el catálogo de herramientas (HTTP ${r.status})`);
|
|
54
|
+
const { herramientas } = await r.json();
|
|
55
|
+
return herramientas;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Sustituye {id} en la ruta y deja el resto como parámetros. */
|
|
59
|
+
function armarLlamada(meta, argumentos) {
|
|
60
|
+
let ruta = meta.ruta;
|
|
61
|
+
const resto = { ...argumentos };
|
|
62
|
+
|
|
63
|
+
for (const trozo of ruta.match(/\{(\w+)\}/g) || []) {
|
|
64
|
+
const clave = trozo.slice(1, -1);
|
|
65
|
+
ruta = ruta.replace(trozo, encodeURIComponent(resto[clave] ?? ""));
|
|
66
|
+
delete resto[clave];
|
|
67
|
+
}
|
|
68
|
+
return { ruta, resto };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const herramientas = await cargarHerramientas();
|
|
72
|
+
const porNombre = new Map(herramientas.map((h) => [h.name, h]));
|
|
73
|
+
|
|
74
|
+
const servidor = new Server(
|
|
75
|
+
{ name: "azur", version: "1.0.0" },
|
|
76
|
+
{ capabilities: { tools: {} } }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
servidor.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
80
|
+
tools: herramientas.map(({ name, description, inputSchema }) => ({
|
|
81
|
+
name,
|
|
82
|
+
description,
|
|
83
|
+
inputSchema,
|
|
84
|
+
})),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
servidor.setRequestHandler(CallToolRequestSchema, async (peticion) => {
|
|
88
|
+
const h = porNombre.get(peticion.params.name);
|
|
89
|
+
if (!h) {
|
|
90
|
+
return {
|
|
91
|
+
isError: true,
|
|
92
|
+
content: [{ type: "text", text: `No existe la herramienta ${peticion.params.name}.` }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const meta = h["x-azur"];
|
|
97
|
+
const { ruta, resto } = armarLlamada(meta, peticion.params.arguments || {});
|
|
98
|
+
|
|
99
|
+
let url = `${API}${ruta}`;
|
|
100
|
+
const opciones = { method: meta.metodo, headers: { ...cabeceras } };
|
|
101
|
+
|
|
102
|
+
if (meta.metodo === "GET") {
|
|
103
|
+
const q = new URLSearchParams(
|
|
104
|
+
Object.entries(resto).filter(([, v]) => v !== undefined && v !== null)
|
|
105
|
+
);
|
|
106
|
+
if (q.toString()) url += `?${q}`;
|
|
107
|
+
} else {
|
|
108
|
+
// Idempotencia: si el modelo repite la llamada porque no entendió la respuesta, no se
|
|
109
|
+
// crea un segundo documento.
|
|
110
|
+
opciones.headers["Idempotency-Key"] = `mcp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
111
|
+
opciones.body = JSON.stringify(resto);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const r = await fetch(url, opciones);
|
|
116
|
+
const cuerpo = await r.json();
|
|
117
|
+
|
|
118
|
+
// Los errores se devuelven como texto legible: el modelo tiene que poder entender qué
|
|
119
|
+
// falló y decidir. Un JSON de error crudo lo despista.
|
|
120
|
+
if (!r.ok || cuerpo.ok === false) {
|
|
121
|
+
const e = cuerpo.error || {};
|
|
122
|
+
let texto = `No se pudo: ${e.mensaje || r.statusText}`;
|
|
123
|
+
if (e.detalle?.motivos) texto += `\n\nMotivos:\n- ${e.detalle.motivos.join("\n- ")}`;
|
|
124
|
+
if (e.reintentable) texto += `\n\n(Se puede reintentar.)`;
|
|
125
|
+
return { isError: true, content: [{ type: "text", text: texto }] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let texto = JSON.stringify(cuerpo.datos, null, 2);
|
|
129
|
+
|
|
130
|
+
// Si la búsqueda dejó varios candidatos, se dice CLARO. Es la diferencia entre ayudar a
|
|
131
|
+
// facturar y facturarle a quien no era.
|
|
132
|
+
if (cuerpo.datos?.unico === false) {
|
|
133
|
+
texto =
|
|
134
|
+
`Hay ${cuerpo.datos.total} resultados: NO se puede dar por bueno ninguno sin ` +
|
|
135
|
+
`preguntar cuál es.\n\n` + texto;
|
|
136
|
+
}
|
|
137
|
+
if (cuerpo.avisos?.length) {
|
|
138
|
+
texto += `\n\nAvisos:\n- ${cuerpo.avisos.join("\n- ")}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { content: [{ type: "text", text: texto }] };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
isError: true,
|
|
145
|
+
content: [{ type: "text", text: `No se pudo hablar con AZUR: ${err.message}` }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await servidor.connect(new StdioServerTransport());
|
|
151
|
+
console.error(
|
|
152
|
+
`AZUR MCP en marcha · ${herramientas.length} herramientas · ` +
|
|
153
|
+
`envío al SRI ${PERMITIR_ENVIO ? "PERMITIDO" : "bloqueado"}`
|
|
154
|
+
);
|