In questo articolo
- 🧭 Aggiungere vs creare: sono due lavori diversi
- 🧩 Anatomia: tool, resource, prompt, transport
- ⚙️ Setup: FastMCP in Python, in 3 comandi
- 🔨 Il primo tool: hello world in stile MCP
- 🏢 Esporre un sistema interno reale (esempio: ordini)
- 🛡️ Sicurezza: cosa NON esporre (e come si autentica)
- 🐛 Errori comuni e debug
- 📦 GitHub repo
- ✅ Checklist finale
🧭 Aggiungere vs creare: sono due lavori diversi
Prima di partire, un chiarimento importante: aggiungere un server MCP esistente e crearne uno tuo sono due lavori molto diversi. Se ti serve solo collegare un server già pronto (Playwright, GitHub, Notion) al tuo Claude Code o Codex, non stai leggendo l'articolo giusto — quelli li ho già coperti negli articoli linkati in fondo.
Qui invece parlo del caso in cui vuoi esporre un sistema tuo a un agente AI: un CRM interno, un database aziendale, una API custom, uno script legacy. Il modo pulito per farlo, oggi, è scrivere un server MCP che parla il Model Context Protocol e lascia che qualunque client compatibile lo consumi allo stesso modo.
Aggiungere (config)
- Un server esiste già (npm/pip/binario)
- Modifichi solo un file di configurazione
- Zero codice tuo, zero manutenzione
- Coperto in: 'Aggiungere MCP a Claude e Codex'
Creare (codice)
- Scrivi tu i tool in Python (o TypeScript)
- Decidi cosa esporre e cosa NO
- Manutieni il codice come qualunque servizio
- È quello di cui parliamo qui
Se non hai già un server pronto per ciò che vuoi fare, tocca scrivertelo. Ed è più veloce di quel che sembra.
🧩 Anatomia: tool, resource, prompt, transport
Un server MCP espone al client tre primitive e vive su un transport. Le primitive sono: tool (azioni che il modello può invocare), resource (dati read-only indirizzati da URI che il modello può leggere) e prompt (template parametrici che l'host può presentare come slash-command).
Il transport è il canale su cui viaggiano i messaggi JSON-RPC: stdio quando il client lancia il server come processo figlio (locale, latenza minima, zero rete) e Streamable HTTP quando il server è remoto e serve più client. Per un primo prototipo si parte quasi sempre da stdio: non devi aprire porte, non devi autenticare, gira tutto sulla tua macchina.
- tool → funzioni con effetti (create, update, query, calcolo).
- resource → dati read-only con un URI (file, tabelle, snapshot).
- prompt → template riutilizzabili con parametri.
- transport → stdio (locale) o Streamable HTTP (remoto).
# @mcp.tool -> l'agente PUÒ invocarlo (ha effetti)
# @mcp.resource -> l'agente PUÒ leggerlo (read-only, per URI)
# @mcp.prompt -> l'host PUÒ presentarlo come slash-command
# Il transport lo decidi all'avvio: stdio (locale) o http (remoto)⚙️ Setup: FastMCP in Python, in 3 comandi
In Python la strada che consiglio nel 2026 è FastMCP — l'implementazione standalone mantenuta da PrefectHQ che oggi copre il 70% dei server MCP in giro. FastMCP 1.0 era stato inglobato nell'SDK ufficiale nel 2024, poi il progetto ha continuato la sua vita da solo ed è quello con l'ergonomia migliore. Se ti serve controllo fine sul wire format, l'SDK ufficiale `mcp` resta un'ottima alternativa; per il 90% dei casi d'uso, FastMCP è più veloce da scrivere e da leggere.
Setup: creo un venv, installo FastMCP e scrivo il primo file. Uso uv (o `pip`, se preferisci) perché installa in un istante e mi tiene il progetto pulito.
# 1) Creo la cartella e il venv
mkdir mcp-orders-demo && cd mcp-orders-demo
uv venv && source .venv/bin/activate
# 2) Installo FastMCP (versione 2.x, stabile in produzione)
uv pip install 'fastmcp>=2.11,<3'
# 3) Creo il file del server
touch server.py🔨 Il primo tool: hello world in stile MCP
Il primo tool serve solo a farsi vedere dal client. Definisco una funzione Python annotata coi tipi, ci metto sopra `@mcp.tool` e FastMCP mi genera da solo il JSON Schema che il modello vede: la docstring diventa la descrizione, i type hint diventano i parametri, il return type diventa il payload di risposta.
Il pattern è sempre lo stesso: ==una funzione con tipi + una docstring chiara == = un tool ben documentato per l'LLM. Se sbagli la docstring o metti tipi vaghi (`Any`, `dict`), l'agente sbaglia a chiamarti. Su questo aspetto vale la pena essere maniacali.
from fastmcp import FastMCP
mcp = FastMCP("orders-demo")
@mcp.tool
def ping(name: str) -> str:
"""Risponde con un saluto. Utile per verificare che il server risponda."""
return f"pong, {name}"
if __name__ == "__main__":
mcp.run() # default: transport stdio🏢 Esporre un sistema interno reale (esempio: ordini)
Adesso il pezzo che conta davvero: collegare un sistema tuo. Nell'esempio del repo espongo un mini store di ordini in-memory (in produzione lo sostituisci con la tua chiamata al DB o alla tua API interna) con due tool — `list_orders` e `get_order` — e una resource `orders://all` che restituisce lo snapshot completo, così il modello può leggerlo senza doverlo invocare esplicitamente.
Nota il pattern: i tool fanno azioni, le resource danno contesto. E soprattutto: non espongo nulla che il modello non deve poter fare. Niente `execute_sql`, niente `delete_all`, niente scorciatoie: se un tool serve solo agli admin, non lo metto nel server MCP.
- Tool discreti e a scopo singolo: `list_orders(status)` sì, `run_query(sql)` no.
- Resource per il contesto: dashboard, riepiloghi, snapshot leggibili.
- Filtro output: ritorna solo i campi che il modello deve vedere.
from fastmcp import FastMCP
from typing import Literal
mcp = FastMCP("orders-demo")
# In produzione: chiamata al tuo DB / alla tua API interna
ORDERS = [
{"id": "A-001", "customer": "Rossi Srl", "total": 1200, "status": "paid"},
{"id": "A-002", "customer": "Bianchi SpA", "total": 340, "status": "pending"},
]
@mcp.tool
def list_orders(status: Literal["paid", "pending"] | None = None) -> list[dict]:
"""Elenca gli ordini, filtrando opzionalmente per stato."""
return [o for o in ORDERS if status is None or o["status"] == status]
@mcp.tool
def get_order(order_id: str) -> dict | None:
"""Ritorna il singolo ordine per id, o None se non esiste."""
return next((o for o in ORDERS if o["id"] == order_id), None)
@mcp.resource("orders://all")
def all_orders() -> list[dict]:
"""Snapshot completo degli ordini (read-only)."""
return ORDERS🛡️ Sicurezza: cosa NON esporre (e come si autentica)
Questa è la parte in cui vedo più errori. A febbraio 2026 i ricercatori di sicurezza segnalavano che il 41% dei server MCP in giro non aveva alcuna forma di autenticazione, e tra gennaio e febbraio 2026 erano già state depositate oltre 30 CVE sul solo ecosistema MCP. Il messaggio è chiaro: un MCP server va trattato come una dipendenza untrusted con permessi root-equivalent.
Regola numero uno: su stdio non serve OAuth, perché il server è locale e fidato; ma qualsiasi credenziale (API key, token DB) la passi da variabili d'ambiente, mai hardcoded. Su HTTP invece l'attuale specifica (2025-11-25) impone OAuth 2.1 con PKCE, e le raccomandazioni operative sono di tenere l'authorization server separato dal resource server, di definire scope per-tool (`orders:read`, `orders:write:paid`) invece di dare all'agente accesso indiscriminato, e di usare token a vita breve con refresh rotanti.
- Mai esporre `run_sql`, `exec_shell`, `read_file(path)` senza allowlist.
- Sempre validare input e limitare gli output (allow-list dei campi).
- HTTP: OAuth 2.1 + PKCE, scope per-tool, token brevi.
- stdio: credenziali via env, mai in chiaro nel codice.
- Log e rate-limit su ogni tool: se un agente parte per la tangente lo vedi subito.
# ❌ NO: dare all'agente una porta di servizio universale
@mcp.tool
def run_sql(query: str) -> list[dict]:
return db.execute(query) # SQL injection + esfiltrazione dati
# ✅ SÌ: tool discreti, tipi stretti, filtri di output
@mcp.tool
def get_paid_orders(limit: int = 20) -> list[dict]:
"""Ultimi ordini pagati (max 100). Ritorna solo campi pubblici."""
limit = max(1, min(limit, 100))
rows = db.orders.find(status="paid", limit=limit)
return [{"id": r.id, "total": r.total} for r in rows]🐛 Errori comuni e debug
I bug che vedo più spesso quando qualcuno scrive il primo server MCP sono quasi sempre gli stessi. Li lascio qui in ordine di frequenza, così se ti capita uno lo riconosci al volo.
Il debug più efficace resta l'MCP Inspector ufficiale (`npx @modelcontextprotocol/inspector`): apri il server, vedi la lista dei tool, invochi quello che vuoi con parametri arbitrari e leggi la risposta. Prima di collegarlo a Claude Code o Codex, fai sempre un giro con l'Inspector: se non funziona lì, non funzionerà nemmeno con l'agente.
- Docstring vuota o vaga → l'agente non capisce quando chiamarlo.
- Type hint `Any` o `dict` → JSON Schema inutile, chiamate sbagliate.
- print() in stdio → rompi il protocollo JSON-RPC. Usa `logging` su stderr.
- Tool troppo generici → l'agente sbaglia sempre. Meglio 5 tool specifici che 1 tuttofare.
- Errori non serializzabili → wrappa sempre in un dict con `error` e `detail`.
# Avvia l'Inspector puntandolo al tuo server stdio
npx @modelcontextprotocol/inspector uv run server.py
# Poi apri http://localhost:6274 e:
# - controlla che i tool compaiano in 'Tools'
# - invoca list_orders con status=paid
# - leggi la risposta e i log JSON-RPCDoc ufficiale · MCP Inspector ↗
📦 GitHub repo
Tutto il codice di questo articolo — server FastMCP funzionante, mini store ordini, esempio HTTP con Bearer e `docker-compose.yml` per provarlo senza installare Python in locale — sta nel repo pubblico cool-solution-org/mcp-server-orders-demo. Cloni, `docker compose up -d` e in trenta secondi hai il server MCP che risponde all'Inspector.
Nel README trovi anche il passo per collegarlo a Claude Code (`claude mcp add`) e la spiegazione riga-per-riga della configurazione `.env.example`. Ogni snippet di questo articolo è preso pari-pari da quel repo, così puoi partire da lì e adattarlo al tuo sistema interno.
git clone https://github.com/cool-solution-org/mcp-server-orders-demo
cd mcp-server-orders-demo
docker compose up -d
# Poi l'Inspector, per vedere che i tool ci sono davvero
npx @modelcontextprotocol/inspector docker compose exec server uv run server.pyRepo GitHub · mcp-server-orders-demo ↗
✅ Checklist finale
Se stai per pubblicare il tuo primo server MCP, prima di darlo in pasto a un agente, passa da questa checklist. Sono le domande che io stesso mi rifiuto di saltare, perché ogni volta che le ho saltate mi sono trovato a rincorrere un bug in produzione.
- Ogni tool ha docstring chiara e tipi stretti (no `Any`, no `dict` sciolti)?
- Nessun tool generico tipo `run_sql`, `exec_shell`, `read_file(path)`?
- Credenziali solo da env, mai committate?
- Se HTTP: OAuth 2.1 + PKCE, scope per-tool, token brevi?
- Log e rate-limit su ogni tool?
- Testato con l'Inspector prima di collegarlo all'agente?
Domande frequenti su creare server MCP custom
Python o TypeScript per scrivere un server MCP?
Vanno bene entrambi: ci sono SDK ufficiali per tutti e due. Io consiglio Python con FastMCP per il 90% dei casi perché è il più veloce da scrivere e da leggere, ed è quello con la community più grande (copre il ~70% dei server MCP in giro). TypeScript ha senso se il tuo sistema è già in Node e vuoi condividere tipi e librerie.
Che differenza c'è tra aggiungere un server MCP e crearne uno?
Aggiungere significa collegare un server già pronto (Playwright, GitHub, Notion) al tuo Claude Code o Codex modificando solo un file di configurazione. Creare significa scrivere tu il codice del server per esporre un tuo sistema (CRM, DB interno, API custom) a un agente. Sono due lavori diversi: se ti basta il primo, non ti serve leggere questo articolo.
Come metto in sicurezza un server MCP?
Regole base: evita tool generici (`run_sql`, `exec_shell`); usa type hint stretti e valida gli input; passa le credenziali da variabili d'ambiente, mai hardcoded. Se il server è HTTP (remoto), la spec 2025-11-25 impone OAuth 2.1 con PKCE, separa authorization server e resource server, definisci scope per-tool e usa token a vita breve. Aggiungi log e rate-limit su ogni tool.
Local (stdio) o remoto (HTTP): quale scelgo?
Parti sempre da stdio per il prototipo: latenza minima, zero autenticazione, zero rete. Passi a Streamable HTTP solo quando il server deve stare su un'altra macchina o servire più utenti insieme — a quel punto ti tocca fare seriamente OAuth 2.1. La logica dei tool non cambia: cambia solo come li avvii.
Come faccio il debug prima di collegare il server all'agente?
Usa l'MCP Inspector ufficiale (`npx @modelcontextprotocol/inspector`): è una UI che si collega al tuo server, ti mostra i tool esposti, ti permette di invocarli con parametri arbitrari e ti fa vedere i messaggi JSON-RPC. Se non funziona nell'Inspector, non funzionerà nemmeno con Claude Code o Codex — quindi partire da lì ti fa risparmiare ore.
Parliamone
Se questo tema ti riguarda, scrivimi: confrontarsi su codice e AI è sempre tempo speso bene.



