Introduzione
Il Model Context Protocol (MCP) è uno standard aperto che permette di collegare in modo sicuro modelli AI e servizi esterni. In questa guida pratica imparerai a creare da zero un client MCP in Python capace di:
- Avviare o connettersi a un server MCP locale o remoto.
- Passare i tool esposti dal server a Claude (o un altro LLM compatibile) — che deciderà se e come usarli.
- Gestire le chiamate ai tool, restituendo all’utente una risposta naturale.
Seguiremo il quickstart ufficiale per client pubblicato sul sito di Model Context Protocol e ne estrarremo i passaggi fondamentali.(modelcontextprotocol.io)
Cos’è MCP e perché serve un client
MCP definisce messaggi strutturati (JSON) per descrivere tool, invocarli e restituire risultati. Un client MCP fa da ponte tra il modello linguistico (Claude, GPT, ecc.) e i server MCP che custodiscono dati o funzionalità:
- Scoperta tool – il client chiede
list_tools
al server. - Conversazione – passa la tua richiesta e la lista tool al modello.
- Esecuzione tool – se il modello decide di usare un tool, il client chiama
call_tool
sul server e rimanda il risultato al modello.
Questa architettura disaccoppia UI / logica AI dal backend che ospita le risorse. Risultato: più sicurezza, riuso dei tool ed evoluzione indipendente dei componenti.(modelcontextprotocol.io)
Requisiti e setup dell’ambiente
Prerequisiti
- Python (versione recente)
- uv per gestire ambiente e dipendenze
- Chiave API Anthropic (Claude)
1. Creazione progetto e virtualenv
# Creazione cartella
uv init mcp-client
cd mcp-client
# Virtual environment
uv venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Dipendenze
uv add mcp anthropic python-dotenv
2. Gestione API Key
touch .env
echo "ANTHROPIC_API_KEY=<la_tua_chiave>" >> .env
echo ".env" >> .gitignore
Struttura base del client
Crea client.py
e incolla il boilerplate seguente.
import asyncio
import os
import sys
from typing import Optional
from contextlib import AsyncExitStack
from dotenv import load_dotenv
from anthropic import Anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
load_dotenv()
class MCPClient:
"""Client MCP minimale con supporto a Claude."""
def __init__(self):
self.session: Optional[ClientSession] = None
self.exit_stack = AsyncExitStack()
self.anthropic = Anthropic()
Connessione al server MCP
async def connect(self, server_script: str):
is_py = server_script.endswith(".py")
is_js = server_script.endswith(".js")
if not (is_py or is_js):
raise ValueError("Il server deve essere .py o .js")
cmd = "python" if is_py else "node"
params = StdioServerParameters(command=cmd, args=[server_script])
stdio = await self.exit_stack.enter_async_context(stdio_client(params))
self.read, self.write = stdio
self.session = await self.exit_stack.enter_async_context(ClientSession(self.read, self.write))
await self.session.initialize()
tools = (await self.session.list_tools()).tools
self.tools = [
{"name": t.name, "description": t.description, "input_schema": t.inputSchema}
for t in tools
]
print("Connesso al server. Tool disponibili:", [t["name"] for t in self.tools])
Elaborazione delle query e chiamate tool
async def process_query(self, query: str) -> str:
messages = [{"role": "user", "content": query}]
# Primo round con i tool a disposizione
response = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
tools=self.tools,
)
final = []
for block in response.content:
if block.type == "text":
final.append(block.text)
elif block.type == "tool_use":
result = await self.session.call_tool(block.name, block.input)
messages += [
{"role": "assistant", "content": [block]},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": block.id,
"content": result.content,
}
],
},
]
# Secondo round dopo il tool
follow_up = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1000,
messages=messages,
)
final.append(follow_up.content[0].text)
return "\n".join(final)
Ciclo chat interattivo & pulizia
async def chat(self):
print("\nMCP Client pronto! ('quit' per uscire)")
while True:
q = input("\nQuery: ").strip()
if q.lower() == "quit":
break
print(await self.process_query(q))
async def close(self):
await self.exit_stack.aclose()
async def main():
if len(sys.argv) < 2:
print("Uso: python client.py <path_server.py|js>")
sys.exit(1)
client = MCPClient()
try:
await client.connect(sys.argv[1])
await client.chat()
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())
Esecuzione del client
uv run client.py path/to/weather_server.py # server Python
uv run client.py path/to/build/index.js # server Node
Il flusso sarà:
- Connessione e discovery dei tool.
- Conversazione con Claude.
- Tool call automatiche.
- Risposta all’utente.(modelcontextprotocol.io)
Best practice e personalizzazioni
- Gestione errori – wrappa ogni
call_tool
intry/except
e logga gli errori. - Cleanup risorse – usa
AsyncExitStack
per chiudere trasporti e sessioni. - Sicurezza – mantieni le chiavi API in
.env
e valida l’input proveniente dai server. - UI avanzata – sostituisci il loop CLI con una GUI (Tkinter) o un frontend web usando websockets.
- Caching – se un tool è costoso, memorizza i risultati per richieste successive identiche.
Conclusione
Hai ora un client MCP funzionante che può parlare con qualsiasi server MCP e arricchire le risposte di Claude con tool personalizzati. Espandi il progetto aggiungendo nuovi tool server‑side o integrando modelli diversi dal solito LLM.