@memori.ai/mcp-crmincloud
v2.2.0
Published
MCP server for TeamSystem CRM in Cloud: companies, opportunities with products, quotes with discounts and PDF download links, live product catalog and read-only activities, with self-verified writes.
Readme
MCP TeamSystem CRM in Cloud
Server MCP per la parte commerciale di TeamSystem CRM in Cloud — anagrafiche, opportunità con i loro prodotti, preventivi (con sconti, revisioni e stampa PDF), catalogo, attività in sola lettura — pensato per essere collegato ad agenti AI tramite un MCP Gateway (es. AIsuru), ma generico: nessun dato specifico di un tenant è hardcoded nel codice, quindi funziona con qualunque azienda su CRM in Cloud.
Pacchetto npm: @memori.ai/mcp-crmincloud · avvio: npx -y @memori.ai/mcp-crmincloud
Il pacchetto pubblicato contiene solo src/: è tutto ciò che serve per far funzionare il
server. Test, collaudo e documentazione tecnica stanno fuori dal pacchetto — vedi l'ultima
sezione.
🧰 I tool esposti (22)
| Gruppo | Tool | Cosa fa |
|---|---|---|
| Consultazione | opzioni_opportunita | Stati, fasi, categorie, motivi di perdita (fasi/categorie/motivi letti dal vivo) |
| Consultazione | opzioni_preventivo | Stadi, pagamenti, modalità di pagamento, unità di misura e template di stampa, dal vivo |
| Anagrafiche | cerca_azienda | Per nome, partita IVA e/o città, verificando che il filtro sia stato applicato |
| Anagrafiche | leggi_azienda | Scheda, contatti collegati, opportunità collegate |
| Anagrafiche | crea_azienda | Ragione sociale, P.IVA e indirizzo, controllo duplicati · dry_run |
| Anagrafiche | aggiorna_azienda | Aggiornamento parziale di ragione sociale, CF, P.IVA e indirizzo completo, con rilettura · dry_run |
| Opportunità | cerca_opportunita | Per stato, nome, azienda, fase, intervalli di data |
| Opportunità | leggi_opportunita | Dossier completo: dati, azienda, prodotti, preventivi e attività collegati |
| Opportunità | crea_opportunita | Crea un'opportunità · dry_run |
| Opportunità | aggiorna_opportunita | Aggiorna uno o più campi (titolo, stato, fase, categoria, commerciale…) · dry_run |
| Opportunità | prodotti_opportunita | Elenca i prodotti collegati all'opportunità |
| Opportunità | salva_prodotto_opportunita | Aggiunge o modifica un prodotto sull'opportunità · dry_run |
| Opportunità | elimina_prodotto_opportunita | Rimuove un prodotto dall'opportunità, verificando la cancellazione |
| Preventivi | cerca_preventivo | Per azienda, opportunità, numero, stadio |
| Preventivi | leggi_preventivo | Numero, stadio, totali, righe (con sconti e unità di misura) |
| Preventivi | crea_preventivo | ⭐ Crea un preventivo con le sue righe, o partendo dai prodotti dell'opportunità · dry_run |
| Preventivi | aggiorna_preventivo | Aggiorna testata e/o sostituisce le righe · dry_run |
| Preventivi | clona_preventivo | Duplica (copia) o crea una revisione (revisione) · dry_run |
| Preventivi | scarica_preventivo | Link PDF firmato, verificato scaricandone i primi byte |
| Prodotti | cerca_prodotto | Cerca nel catalogo letto dal vivo dal tuo tenant |
| Prodotti | leggi_prodotto | Scheda di un singolo prodotto |
| Attività | cerca_attivita | Sola lettura, per opportunità e/o azienda |
Il giro completo che un agente può fare da zero: crea_azienda → crea_opportunita →
salva_prodotto_opportunita → crea_preventivo (daProdottiOpportunita: true) →
scarica_preventivo. Ogni risposta include anche il link alla scheda sul CRM, così l'utente
può aprire e controllare quello che l'agente ha fatto — ma quel link lo calcola il server
(§ sotto), non lo restituisce l'API.
Comportamenti garantiti dal server:
- Ogni scrittura si rilegge e si confronta. L'API di CRM in Cloud risponde
200 OKe può scartare campi in silenzio: i campi persi finiscono incampiPersi, e il tool non dichiara successo pieno se la lista non è vuota. - Le righe si validano prima di qualunque chiamata HTTP, e gli errori vengono segnalati tutti insieme invece che uno alla volta.
- Mai indovinare un'unità di misura. Le unità si leggono dal vivo dal CRM: se un prodotto non ne dichiara una a catalogo, il tool si ferma e chiede quale usare invece di scegliere.
- Mai dedurre un prezzo. I prezzi a catalogo sono quasi tutti a zero.
- Le aziende e le opportunità si individuano per id, mai per nome: le ricerche per nome
vengono verificate rileggendo i risultati, perché l'API a volte ignora in silenzio un
filtro sbagliato e risponde comunque
200con una lista qualsiasi. - Gli aggiornamenti inviano solo i campi passati: il CRM fa un merge parziale vero su un
record esistente, i campi non richiesti restano intatti. La scrittura viene comunque
riletta per intero, per accorgersi di qualunque effetto collaterale (es. chiudere
un'opportunità aggiorna anche
closeDatea oggi: atteso, segnalato innota). - Il rate limit (40 chiamate/min) è condiviso fra processi, con una finestra scorrevole persistita su file: più agenti attivi insieme non lo sforano.
- Nessun dato di un tenant specifico è nel codice. Fasi, categorie, motivi di perdita, pagamenti, modalità di pagamento e catalogo prodotti si leggono dal vivo dal CRM di chi usa il server (cache locale di pochi minuti/un'ora, mai spedita nel pacchetto).
📇 Catalogo, unità di misura e prezzi
cerca_prodotto/crea_preventivo leggono esistenza, nome, prezzo, categoria, aliquota e
unità dichiarata dei prodotti dal vivo dal catalogo del tuo tenant (Catalog/Search, cache
di 10 minuti), e le unità di misura dall'anagrafica del CRM (AlternativeUnit/Search):
tutto questo funziona da subito, per qualunque tenant, senza configurazione né file di dati.
Nelle righe l'unità si passa per nome (unitaMisura: "mesi") oppure per id, e viene
risolta contro l'elenco vero: un nome inesistente viene rifiutato elencando quelli validi.
Se il prodotto ne dichiara una a catalogo, quella è il default.
Esiste anche uno storico locale opzionale (out/contratto-prodotti.json, generabile con uno
script in others/, mai committato perché contiene prezzi e quantità reali): serve solo a
recuperare tipo riga e aliquota per prodotti mai usati, e il server funziona identico senza.
💸 Sconti, note e commerciale
- Sconto su un prodotto specifico:
scontoFormulasulla riga, es."10"oppure"10+5"per sconti in cascata.prezzoUnitarioresta il prezzo pieno e il netto lo calcola il CRM, così la stampa mostra listino, sconto e netto. Se il prodotto ha uno sconto massimo a catalogo, superarlo è un errore. (Passare un listino diverso dal prezzo senza formula viene rifiutato: il CRM lo annullerebbe in silenzio.) - Sconto sul totale: una riga senza codice prodotto, con la sola descrizione e
prezzoUnitarionegativo. Eredita l'aliquota IVA delle righe prodotto, altrimenti scontarebbe l'IVA sbagliata. - Note del preventivo: una riga con
testo(HTML ammesso), che diventa una riga puramente descrittiva senza prezzo. - Commerciale: per default è l'utente della
CRM_API_KEY, ma è sempre modificabile —commerciale(email o id utente) sucrea_azienda,crea_opportunita,aggiorna_opportunita,crea_preventivoeaggiorna_preventivo.
🔗 I link alle schede: calcolati, non forniti dall'API
L'API di CRM in Cloud non restituisce le URL dell'interfaccia web, e quelle URL non usano l'id
numerico ma un id offuscato (/opportunities/8Xlw678O). I link nelle risposte li calcola questo
server dall'id, riproducendo lo schema del frontend React attuale: hashids con salt
hkjt753gynbv e lunghezza minima 8. La configurazione è stata letta direttamente dal
RouteHelper del bundle React e verificata sulla coppia reale opportunità
710918 → 8Xlw678O.
La forma della rotta dipende dall'entità: le opportunità usano /opportunities/{hash}, le
aziende /companies/{hash}/company, mentre i preventivi — che nel frontend non hanno una
modalità di sola vista — usano /quotes/edit/{hash}/quote.
Siccome è uno schema letto dall'interfaccia e non un contratto pubblico, ogni risposta che contiene
link porta anche un campo linkNota che lo dichiara: se un link non apre la scheda giusta,
il riferimento certo è l'id numerico, che c'è sempre. In quel caso:
CRM_LINK_SALT=... # solo se l'interfaccia usa uno schema diverso dal default
CRM_LINK_MIN_LENGTH=8
CRM_LINKS=off # oppure niente link: restano gli id🔌 Configurazione nell'MCP Gateway
Il gateway esegue il server come sottoprocesso e gli inietta i parametri come variabili d'ambiente: il nome del parametro diventa letteralmente il nome della variabile. Le credenziali non passano mai dall'LLM.
Tipo server: Server MCP Generico (generic) · Comando: npx -y @memori.ai/mcp-crmincloud
| Parametro | Obbligatorio | Cosa |
|---|---|---|
| CRM_API_KEY | sì | Web API Key (SETUP → USERS → utente → Web API Keys). Non scade. Determina l'utente proprietario dei record creati |
| CRM_BASE_URL | no | Default https://api.crmincloud.it/api/v1. Non usare /api/latest |
| CRM_RATE_LIMIT | no | Chiamate al minuto. Default 33, il limite reale del CRM è 40 |
| CRM_APP_URL | no | Base dei link alle schede. Default https://app.crmincloud.it |
| CRM_LINKS | no | off per non generare link alle schede (§ sotto) |
| CRM_LINK_SALT · CRM_LINK_MIN_LENGTH | no | Schema degli id nelle URL dell'interfaccia. Default: salt hkjt753gynbv, lunghezza 8 |
Autenticazione: un solo service account condiviso. Il gateway inietta le credenziali
per istanza del server, non per sessione utente: quindi CRM_API_KEY è la Web API Key di
un unico utente "tecnico", e i record creati dall'MCP nascono con quell'utente come
ownerId/salesPerson. Il commerciale però si può passare esplicitamente su ogni scrittura
(parametro commerciale), quindi l'attribuzione corretta non richiede più un'istanza per
persona.
Il perimetro di ciò che l'MCP può toccare è quello della chiave: la Web API Key si può limitare per operazione (SETUP → USERS → Web API Keys), ed è lì che va impostato un eventuale vincolo, non nel server.
Nota sul rate limit. Il gateway crea e uccide un processo a ogni chiamata di tool, quindi un freno tenuto in memoria non conterebbe nulla. La finestra scorrevole è persistita su file temporaneo, condivisa fra tutti i processi che usano la stessa chiave. Se il budget è esaurito il tool fallisce subito con un messaggio chiaro, invece di restare appeso finché il gateway lo uccide.
🗂 Struttura di questo repository
| Percorso | Cosa | Serve per far girare il server? |
|---|---|---|
| src/ | Il server MCP vero e proprio (i 22 tool, client HTTP, catalogo, lookup, link) | sì — assieme a package.json è l'unica cosa pubblicata su npm |
| package.json / package-lock.json | Metadati e dipendenze npm | sì |
| out/ | Cache e storico opzionale, generati a runtime, mai committati né pubblicati | no |
Il pacchetto pubblicato è autosufficiente: npx -y @memori.ai/mcp-crmincloud scarica
src/ più le tre dipendenze e parte. Nessun file di test, di documentazione o di dati serve
in produzione.
🧪 Sviluppo, test e documentazione tecnica
Tutto ciò che serve per sviluppare, testare e collaudare il server (non per farlo
funzionare) sta fuori dal pacchetto, in una cartella others/ separata:
docs/— documentazione tecnica: riferimento API misurato, insidie note dell'API CRM in Cloud, specifica dei tool, come si dimostra che il collaudo funzionaprova-mcp.mjs— test suite: protocollo, schema dei tool, guardie sui parametri (con--reteaggiunge letture dal vivo, senza scrivere niente)collaudo.mjs,riverifica.mjs— collaudo end-to-end contro un CRM reale: ricrea preventivi storici e li confronta byte per byte, più il giro commerciale completogenera-contratto.mjs,congela-fixture.mjs,crea-azienda-test.mjs,verifica-duplicati.mjs,crm-client.mjs,percorsi.mjs— script di supporto.env.example— configurazione per gli script sopra (copiala in.env)
cd others
cp .env.example .env # poi compila CRM_API_KEY
node prova-mcp.mjs # nessuna chiamata al CRM
node prova-mcp.mjs --rete # aggiunge letture/ricerche dal vivo
node collaudo.mjs # giro commerciale completo, scrive su un'azienda di testGli script trovano il pacchetto da soli, sia che others/ stia dentro la cartella dell'MCP sia
che stia accanto. Se lo tieni in un posto qualsiasi, indicaglielo con MCP_DIR:
MCP_DIR="/percorso/della/cartella/MCP CRMinCloud" node collaudo.mjs