Da programmatore, una delle cose che mi interessa maggiormente dell'intelligenza artificiale non è la capacità di generare codice in pochi secondi, ma la possibilità di utilizzarla per automatizzare attività concrete. Scrivere una funzione, spiegare un algoritmo o suggerire una soluzione sono operazioni utili, ma il vero salto di qualità arriva quando il modello può interagire direttamente con gli strumenti che utilizzo ogni giorno, lavorare sui miei file e portare a termine un'attività senza costringermi a eseguire manualmente ogni singolo passaggio.
È proprio questo il motivo per cui sto sperimentando con OpenCode e i server MCP. Voglio costruire un ambiente nel quale l'intelligenza artificiale non sia soltanto un assistente con cui discutere di programmazione, ma uno strumento operativo capace di coordinare applicazioni differenti. Un sistema in grado di leggere dati, modificarli, produrre documenti e verificare i risultati, mantenendo il controllo del processo.
Il 28 ottobre 2026 ho utilizzato questo approccio per gestire un'attività legata a un corso di formazione WordPress + Elementor. Partendo da due file Excel e da un modello Word già esistente, ho automatizzato l'elaborazione dei nominativi, la compilazione degli attestati e la generazione del manuale utente. Il risultato finale è stato di 57 e-mail inviate con allegati manuale e attestato personale.
La parte interessante non è soltanto aver prodotto decine di documenti senza compilarli manualmente uno alla volta. È aver combinato diversi strumenti specializzati in un unico flusso di lavoro, facendo svolgere a ciascuno il compito per cui è stato predisposto. In questo articolo racconto anche due estensioni: un server MCP che ho scritto io per l'invio delle email e il manuale utente del software, che ho fatto generare a un agente.
Che cosa sono gli MCP Server e perché li utilizzo con OpenCode
MCP significa Model Context Protocol. Si tratta di un protocollo che permette ai modelli di intelligenza artificiale di accedere a strumenti e risorse esterne attraverso interfacce definite. In pratica, un modello può richiamare le funzionalità esposte da un server MCP, ricevere i risultati delle operazioni e utilizzarli per proseguire il lavoro.
Senza strumenti collegati, un modello può spiegarmi come modificare un foglio Excel o generare un documento PDF. Con gli strumenti appropriati, può invece intervenire direttamente sui file, leggere le informazioni necessarie e coordinare le operazioni richieste.
Nel mio ambiente utilizzo OpenCode come interfaccia per lavorare con diversi server MCP. Ho scelto di suddividere le funzionalità in strumenti specializzati, invece di concentrare tutto in un'unica applicazione. Così posso riutilizzare le capacità dei singoli server in attività differenti, senza ricostruire ogni volta l'intero sistema.
Per questo lavoro ho utilizzato quattro componenti: GridCraft per i fogli di calcolo, WordCraft per i documenti Word, PDFCraft per l'analisi dei PDF e un server email scritto da me. Naturalmente gli MCP Server non rendono automaticamente corretto qualsiasi risultato: il modello deve comunque interpretare i dati, scegliere le operazioni appropriate e verificare ciò che produce. La differenza è che può farlo interagendo con l'ambiente di lavoro reale, anziché limitarsi a suggerire istruzioni da eseguire a mano.
Il punto di partenza: due file Excel con informazioni complementari
Nella directory di lavoro erano presenti due documenti: il calendario delle formazioni e l'elenco dei partecipanti con i relativi indirizzi email. Il primo conteneva le informazioni sui corsi, comprese date e indicazioni sul luogo di svolgimento; il secondo raccoglieva i nominativi da utilizzare per gli attestati.
Le informazioni necessarie erano quindi già disponibili, ma distribuite tra due documenti. Prima di produrre gli attestati bisognava leggere i dati, capire come fossero organizzati e ricondurli a una struttura coerente. Per questa fase ho utilizzato GridCraft, che mi ha permesso di ispezionare i workbook e leggere gli intervalli di celle interessati, senza aprire manualmente ogni file e copiare i dati a mano.
Molte attività amministrative non richiedono algoritmi sofisticati: richiedono di raccogliere informazioni che esistono già, confrontarle e usarle nel formato corretto. Il problema, quando lo si fa manualmente, è che ogni passaggio introduce la possibilità di un errore: un nominativo copiato male, una data associata alla persona sbagliata, un luogo indicato in modo impreciso. Automatizzare riduce queste operazioni ripetitive, a condizione di predisporre controlli adeguati.
Incrociare i dati e normalizzare il luogo della formazione
Una delle difficoltà riguardava il campo con data e luogo della formazione, scritto in forma testuale e non sempre nello stesso modo. Una persona che conosce il contesto lo capisce al volo, ma per inserirlo negli attestati serviva ricondurre ogni descrizione a una denominazione coerente, evitando che lo stesso luogo comparisse con diciture diverse.
Ho quindi usato una semplice logica di mappatura testuale: le descrizioni venivano analizzate e associate alla denominazione standard corrispondente, mentre i casi non riconosciuti venivano segnalati per una verifica. L'elaborazione ha coinvolto 67 nominativi; sette sono stati esclusi dalla generazione perché privi di una data valorizzata nella colonna del giorno del corso.
Questo passaggio evidenzia un principio che considero fondamentale: automatizzare non significa elaborare tutti i dati disponibili, ma anche riconoscere quelli incompleti. Quando manca un'informazione indispensabile, è meglio escludere il record o segnalarlo piuttosto che produrre un documento potenzialmente errato.
Una volta normalizzati i luoghi, ho aggiornato il foglio dei nominativi aggiungendo una colonna dedicata. Il foglio aggiornato può essere consultato anche indipendentemente dall'automazione, e i dati diventano più semplici da usare nelle fasi successive.
WordCraft: trasformare un modello Word in decine di attestati personalizzati
Invece di creare ogni documento da zero, ho utilizzato un file Word già predisposto, con impaginazione, testo e informazioni statiche del corso. I campi variabili (nominativo, data della formazione e luogo) erano identificati da segnaposto testuali sostituibili con i valori recuperati dal foglio Excel. WordCraft, il server MCP dedicato ai documenti Word, ha aperto il modello, sostituito i campi e preparato i documenti per i singoli partecipanti.
Usare un modello esistente è importante: in un contesto lavorativo il documento finale deve rispettare una struttura grafica e testuale precisa. Con questo approccio il modello resta il punto di riferimento per l'impaginazione e l'automazione si occupa solo dei dati che cambiano. Se in futuro servisse modificare il testo o la grafica, basterebbe intervenire sul modello senza riprogettare il processo. La stessa impostazione vale per lettere personalizzate, certificazioni, comunicazioni amministrative e report periodici.
Da Word a PDF: la generazione dei 57 attestati
Per ogni partecipante selezionato, il sistema ha inserito il nominativo corretto, riportato la data del corso nel formato italiano, indicato il luogo e prodotto un PDF con un nome riconoscibile, basato sul nominativo della persona. Il risultato è stato di 57 attestati PDF.
Il risparmio di tempo è solo uno degli obiettivi: ridurre le operazioni manuali significa anche diminuire le occasioni in cui un dato viene copiato male o un campo dimenticato. Ma il vantaggio dipende dalla qualità dei controlli: ripetere automaticamente un errore non lo rende meno grave, lo rende soltanto più veloce. Per questo considero la generazione una fase del processo, non il suo punto di arrivo.
PDFCraft: verificare il risultato dopo la generazione
Dopo la produzione dei PDF ho usato PDFCraft per controllarne il contenuto, estraendo il testo senza dover aprire ogni file in un visualizzatore. Ho verificato la corrispondenza tra nominativo e documento, la coerenza della data nella tabella con quella nel testo e la correttezza della data finale di emissione. Va distinta la data della formazione, cioè il giorno in cui il partecipante ha seguito il corso, dalla data in calce, che indica l'emissione dell'attestato: nel caso specifico la riga finale riportava "Roma, 28 ottobre 2026".
La verifica del testo permette anche di individuare segnaposto rimasti nel documento o campi vuoti. Non garantisce però che l'impaginazione sia perfetta: un PDF può contenere tutti i dati corretti e avere comunque una tabella disallineata. Per un processo da eseguire regolarmente affiancherei quindi controlli visivi a campione. Il modello Word originale, infine, è rimasto invariato, mantenendo una separazione netta tra template e documenti prodotti.
Il server MCP che ho scritto io: invio email con SMTP
Generati e verificati i PDF, restava l'ultimo passaggio: farli arrivare ai partecipanti. Il secondo file Excel conteneva già gli indirizzi, quindi serviva solo uno strumento per spedire. Invece di cercare una soluzione già pronta, ho scritto un piccolo server MCP in Python con l'SDK ufficiale. Espone due tool: send_email, per inviare messaggi con eventuali allegati, e status, per sapere da quale indirizzo si parte, se si è in modalità di prova, quanti messaggi sono già partiti e qual è stato l'ultimo errore.
Ho dato priorità alla sicurezza e alla prevedibilità, perché un modello che può inviare email è uno strumento potente e va tenuto sotto controllo:
- Whitelist dei domini destinatari (ALLOWED_DOMAINS): il server rifiuta indirizzi fuori elenco.
- Directory consentite per gli allegati (ATTACH_ROOTS): il modello non può allegare file qualsiasi del disco, solo quelli nelle cartelle che decido io.
- Limite di dimensione totale degli allegati (MAX_ATTACH_MB).
- Modalità DRY_RUN: simula l'invio senza spedire nulla, utilissima per provare il flusso prima di partire sul serio.
- Cadenza degli invii (delay_s): i provider come Gmail bloccano gli invii troppo ravvicinati, quindi il server suggerisce al modello di distanziare i messaggi e di non ritentare in loop in caso di blocco.
Le credenziali non sono nel codice: arrivano da un file .env accanto allo script.
import mimetypes
import os
import smtplib
import ssl
import time
from datetime import datetime
from email.message import EmailMessage
from pathlib import Path
from dotenv import load_dotenv
from mcp.server.mcpserver import MCPServer
# carica .env accanto a server.py (le variabili già presenti nell'ambiente vincono)
load_dotenv(Path(__file__).with_name(".env"))
SMTP_HOST = os.environ["SMTP_HOST"]
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER = os.environ["SMTP_USER"]
SMTP_PASS = os.environ["SMTP_PASS"].replace(" ", "") # le app password Google si copiano con spazi
FROM_ADDR = os.environ.get("FROM_ADDR", SMTP_USER)
# whitelist opzionale: "example.com,gmail.com"
ALLOWED_DOMAINS = [d.strip().lower() for d in os.environ.get("ALLOWED_DOMAINS", "").split(",") if d.strip()]
DRY_RUN = os.environ.get("DRY_RUN", "0") == "1"
# limite dimensione totale allegati (Gmail rifiuta oltre ~25 MB)
MAX_ATTACH_MB = float(os.environ.get("MAX_ATTACH_MB", "20"))
# se valorizzato, gli allegati devono stare dentro queste directory (csv): "/home/utente/Documenti,/tmp"
ATTACH_ROOTS = [Path(d.strip()).expanduser() for d in os.environ.get("ATTACH_ROOTS", "").split(",") if d.strip()]
MCP_PORT = int(os.environ.get("MCP_PORT", "8765"))
# registro della sessione (in memoria): usato da status()
SENT: list[dict] = []
LAST_ERROR: str | None = None
_roots_txt = ", ".join(str(p) for p in ATTACH_ROOTS) if ATTACH_ROOTS else "nessuna restrizione"
_domains_txt = ", ".join(ALLOWED_DOMAINS) if ALLOWED_DOMAINS else "nessuna restrizione"
_mode_txt = "DRY_RUN attivo: nessun invio reale" if DRY_RUN else "invii reali"
INSTRUCTIONS = f"""Server email — invia messaggi via SMTP ({SMTP_HOST}:{SMTP_PORT}) \
con mittente {FROM_ADDR}.
Stato all'avvio: {_mode_txt}.
Allegati: massimo {MAX_ATTACH_MB:g} MB totali per messaggio; il MIME è dedotto dall'estensione.
Percorsi allegati consentiti: {_roots_txt}.
Domini destinatari consentiti: {_domains_txt}.
Uso consigliato:
- chiama `status()` per lo stato a runtime, i contatori della sessione e l'ultimo errore;
- per invii multipli cadenza i messaggi: massimo ~20-25 email/ora, una ogni 30-60 secondi
(parametro `delay_s` di send_email), e tieni gli allegati sotto ~2 MB quando possibile;
- in caso di blocco SMTP (Gmail: "sending limit", "unusual activity") NON ritentare in loop:
attendi almeno un'ora (fino a 24 nei casi peggiori) e riprova con un solo messaggio leggero;
- `to` accetta più indirizzi ma restano tutti visibili: per invii multipli usa una chiamata
per destinatario, oppure `bcc`.
"""
mcp = MCPServer("email", instructions=INSTRUCTIONS)
def _check_domains(addrs: list[str]) -> None:
if not ALLOWED_DOMAINS:
return
for a in addrs:
if a.rsplit("@", 1)[-1].lower() not in ALLOWED_DOMAINS:
raise ValueError(f"Dominio non consentito: {a}")
def _resolve_attachment(raw: str) -> Path:
p = Path(raw).expanduser()
if not p.is_absolute():
p = Path.cwd() / p
p = p.resolve()
if ATTACH_ROOTS and not any(p.is_relative_to(root.resolve()) for root in ATTACH_ROOTS):
raise ValueError(f"Allegato fuori dalle directory consentite (ATTACH_ROOTS): {p}")
if not p.is_file():
raise ValueError(f"Allegato non trovato: {p}")
return p
def _add_attachments(msg: EmailMessage, paths: list[str]) -> list[Path]:
added: list[Path] = []
total = 0
for raw in paths:
p = _resolve_attachment(raw)
data = p.read_bytes()
total += len(data)
if total > MAX_ATTACH_MB * 1024 * 1024:
raise ValueError(f"Allegati oltre il limite totale di {MAX_ATTACH_MB:g} MB (MAX_ATTACH_MB)")
ctype = mimetypes.guess_type(p.name)[0] or "application/octet-stream"
maintype, _, subtype = ctype.partition("/")
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=p.name)
added.append(p)
return added
@mcp.tool()
def status() -> dict:
"""Stato del server: mittente, modalità (reale o dry run), limiti e contatori della sessione.
Usalo prima di un invio massivo per sapere da quale indirizzo si parte, se gli invii
sono reali, quanti messaggi sono già partiti e quale è stato l'ultimo errore SMTP.
"""
today = datetime.now().date()
sent_today = [s for s in SENT if s["at"].date() == today and s["ok"]]
return {
"from": FROM_ADDR,
"smtp": f"{SMTP_HOST}:{SMTP_PORT}",
"dry_run": DRY_RUN,
"max_attach_mb": MAX_ATTACH_MB,
"attach_roots": [str(p) for p in ATTACH_ROOTS],
"allowed_domains": ALLOWED_DOMAINS,
"sent_total": len([s for s in SENT if s["ok"]]),
"sent_today": len(sent_today),
"failed_total": len([s for s in SENT if not s["ok"]]),
"last_error": LAST_ERROR,
"recent": SENT[-5:],
"pacing_hint": "max ~20-25 email/ora, una ogni 30-60 s, allegati < ~2 MB",
}
@mcp.tool()
def send_email(
to: list[str],
subject: str,
body: str,
cc: list[str] | None = None,
bcc: list[str] | None = None,
html: bool = False,
attachments: list[str] | None = None,
delay_s: float = 0.0,
) -> str:
"""Invia una email via SMTP.
`html=True` se il body è HTML.
`attachments` è una lista di percorsi di file da allegare (assoluti, o relativi
alla working directory del server); il MIME è dedotto dall'estensione e il totale
non può superare MAX_ATTACH_MB (vedi `status()`).
`delay_s` attende N secondi prima dell'invio: serve a cadenzare gli invii multipli
(consigliato 30-60 s, massimo ~20-25 email/ora) per non far scattare i blocchi Gmail.
`to` accetta più indirizzi, ma restano visibili a tutti: per invii multipli usa una
chiamata per destinatario oppure `bcc`.
"""
global LAST_ERROR
cc, bcc, attachments = cc or [], bcc or [], attachments or []
_check_domains(to + cc + bcc)
if delay_s and delay_s > 0:
time.sleep(delay_s)
msg = EmailMessage()
msg["From"] = FROM_ADDR
msg["To"] = ", ".join(to)
if cc:
msg["Cc"] = ", ".join(cc)
msg["Subject"] = subject
if html:
msg.set_content("Questo messaggio richiede un client HTML.")
msg.add_alternative(body, subtype="html")
else:
msg.set_content(body)
attached = _add_attachments(msg, attachments) # dopo set_content: così resta multipart/mixed
entry = {
"at": datetime.now(),
"to": to + cc + bcc,
"subject": subject,
"attachments": [p.name for p in attached],
"ok": False,
}
if DRY_RUN:
entry["ok"] = True
SENT.append(entry)
extra = f" con allegati {[p.name for p in attached]}" if attached else ""
return f"[DRY RUN] a {to} oggetto '{subject}'{extra}"
try:
ctx = ssl.create_default_context()
if SMTP_PORT == 465:
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ctx, timeout=30) as s:
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg, to_addrs=to + cc + bcc)
else:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as s:
s.starttls(context=ctx)
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg, to_addrs=to + cc + bcc)
except Exception as exc:
LAST_ERROR = f"{datetime.now().isoformat(timespec='seconds')} {type(exc).__name__}: {exc}"
SENT.append(entry)
raise
entry["ok"] = True
SENT.append(entry)
extra = f" con allegati {[p.name for p in attached]}" if attached else ""
return f"Email inviata a {', '.join(to + cc + bcc)}{extra}"
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=MCP_PORT)Un dettaglio che ho curato: nelle istruzioni del server ho scritto i limiti di invio e le regole di cadenza. Il modello le legge quando si collega e le rispetta senza che debba ripeterle io a ogni richiesta. Le istruzioni di un server MCP sono un ottimo posto dove mettere le "regole della casa" di uno strumento.
Il server ascolta solo su 127.0.0.1 con trasporto streamable-http: è raggiungibile solo dalla mia macchina, non esposto in rete. Per collegarlo a OpenCode basta dichiararlo come server remoto nella configurazione:
{
"mcp": {
"email": {
"type": "remote",
"url": "http://127.0.0.1:8765/mcp",
"enabled": true
}
}
}Tenerlo sempre attivo con systemd
Un server che ascolta su una porta deve essere sempre in esecuzione, altrimenti OpenCode non lo trova. Lanciarlo a mano in un terminale ogni volta è scomodo e fragile, quindi ho creato un servizio systemd in modalità utente, che non richiede privilegi di root. Il file è ~/.config/systemd/user/email-mcp.service:
[Unit]
Description=Server MCP email
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/mcp/email
ExecStart=%h/mcp/email/.venv/bin/python server.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.targetIl servizio usa l'interprete del virtualenv, parte nella cartella del progetto (così server.py trova il suo .env) e viene riavviato automaticamente dopo 5 secondi se si interrompe. Lo attivo così:
systemctl --user daemon-reload
systemctl --user enable --now email-mcp.service
# stato e log in tempo reale
systemctl --user status email-mcp.service
journalctl --user -u email-mcp.service -f
# per farlo partire al boot anche senza login interattivo
loginctl enable-linger $USERUna piccola attenzione finale: il .env contiene la password SMTP, quindi vale la pena limitarne i permessi con chmod 600 .env. E prima del primo invio reale conviene lasciare DRY_RUN=1 e far girare l'intero flusso a vuoto: status() conferma cosa sarebbe partito, senza che parta nulla.
Il manuale utente: lo ha scritto un agente
Questo flusso mi ha fatto venire in mente un'altra attività che di solito viene rimandata all'infinito: la documentazione per l'utente. Stavolta ho voluto vedere fino a che punto potevo delegare tutto a un agente, senza scrivere io nemmeno una riga di codice.
Ho affidato il compito a un agente basato su DeepSeek, collegato agli stessi strumenti. Gli ho chiesto di analizzare il software, leggendone codice e struttura, per capire quali funzioni espone, quali sono i percorsi principali e cosa deve fare un utente per ciascuna operazione.
Per le schermate l'agente ha usato Playwright, che pilota un browser reale. Ha scelto da solo come percorrere le sezioni dell'applicazione e quali passaggi catturare. Io non ho scritto né configurato nulla: ho solo descritto l'obiettivo.
Con la struttura funzionale e gli screenshot a disposizione, è entrato in gioco WordCraft. L'agente ha costruito il manuale in Word: indice, un capitolo per ogni funzione, istruzioni passo-passo scritte per un utente non tecnico e gli screenshot inseriti nei punti giusti, con le didascalie. Il risultato è un documento completo, che altrimenti avrei dovuto scrivere e impaginare a mano, schermata per schermata.
Anche qui la revisione resta umana: ho letto il manuale per controllare che le istruzioni corrispondessero al comportamento reale e che gli screenshot non mostrassero dati sensibili. Il vantaggio è che, quando il software cambia, rigenerare le schermate e aggiornare i capitoli diventa un'operazione di pochi minuti invece di un lavoro da rifare da capo.
Perché preferisco combinare più strumenti specializzati
L'esperienza mi ha confermato il valore di un'architettura nella quale ogni server MCP ha una responsabilità precisa. GridCraft si occupa dei fogli di calcolo, WordCraft dei documenti Word, PDFCraft della verifica dei PDF e il mio server email dell'invio. OpenCode mette questi strumenti a disposizione del modello, che li coordina in funzione dell'obiettivo.
Separare le responsabilità permette di cambiare una parte senza toccare le altre. Se domani cambiasse il modello dell'attestato, la parte di lettura dei dati resterebbe invariata. Se volessi usare un altro provider per le email, basterebbe modificare il server di invio. Ed è lo stesso motivo per cui, dopo gli attestati, ho potuto riutilizzare WordCraft per il manuale utente: lo strumento era già lì, mi è bastato cambiare il compito.
Questo non significa che l'automazione sia priva di complessità. Bisogna conoscere gli strumenti, verificare le operazioni eseguite e gestire le situazioni anomale. L'intelligenza artificiale non sostituisce la progettazione del processo: rende possibile interagire con esso in modo più flessibile.
Conclusioni: l'automazione utile è quella che arriva al risultato
Sono partito da due file Excel, ho incrociato i dati dei partecipanti con le informazioni sui corsi, normalizzato i luoghi, aggiornato il workbook, compilato un modello Word e generato 57 attestati PDF. Con PDFCraft ho verificato il contenuto dei documenti, con il server email scritto da me li ho fatti arrivare ai destinatari e con un servizio systemd ho reso lo strumento sempre disponibile. Con un agente DeepSeek, Playwright e WordCraft ho prodotto anche il manuale utente del software.
Non tutte le attività devono essere automatizzate e non tutte richiedono l'intelligenza artificiale. Quando una procedura è completamente deterministica, uno script tradizionale può essere la soluzione più semplice. Quando invece bisogna interpretare informazioni, usare strumenti differenti e adattare le operazioni al contesto, l'integrazione tra un modello e i server MCP offre possibilità interessanti.
È questo il tipo di automazione che voglio continuare a esplorare: meno attività ripetitive svolte manualmente, più strumenti collegati tra loro e maggiore attenzione alla qualità del risultato finale. Non per automatizzare tutto a prescindere, ma per dedicare il tempo risparmiato alle attività in cui l'intervento umano fa davvero la differenza.