Aggiungere un server MCP ad un progetto esistente

Ciao a tutti,

Oggi voglio parlarvi di un progetto interessante che mi ha fatto riflettere molto su cosa significa innovare e modernizzare un’applicazione.

Di recente ho partecipato al Codemotion 2025 a Milano (un evento tech molto ben organizzato qui Italia) e ho ascoltato un breve talk di OpenApi che parlava delle sfide che stavano affrontando nel creare dei server MCP che esponessero le stesse funzionalità delle loro API Rest. Detto così non sembra molto complesso, se ho già la logica di business scritta, è così difficile aggiungere un nuovo progetto a livello delle API? Non dovrebbe.

Non dovrebbe, vero?

Obiettivo

Volendo replicare quanto fatto da OpenApi ho preso un mio progetto (delle API Rest di integrazione) che avevo sviluppato per il mio HomeLab e ho aggiunto un server MCP in parallelo alle API.

I vincoli che mi sono posto erano i seguenti:

  • Dovevo mantenere sia il progetto API che il progetto MCP
  • I due progetti dovevano essere indipendenti l’uno dall’altro
  • I due progetti dovevano funzionare anche in modalità “standalone”, senza che fossero entrambi avviati

Cosa è un server MCP

Il concetto di server MCP dovrebbe essere entrato nella quotidianità di ogni sviluppatore, ma vediamo comunque di ripassarlo. A fine 2024 Anthropic, azienda del famoso modello Claude, inventò il protocollo MCP, uno “standard” su come andare ad integrare una qualsiasi applicazione del mondo “esterno” con i modelli LLM, sempre più diffusi ed utilizzati. Ad oggi questo protocollo è riconosciuto anche da altri big player del mondo AI come OpenAI, Google e Microsoft.

A differenza di un’API REST, che si aggiunge tramite un indirizzo ed una porta, un server MCP dialoga con il client MCP sfruttando il protocollo JSON-RPC 2.0, una sorta di comunicazione continua con messaggi json su “canali” di input e output (std-in e std out).

L’architettura conta

Il progetto era scritto in .NET 10 ed implementava un’architettura clean:

La logica di business è in Application, in infrastructure troviamo i repository ed i moduli di accesso a servizi esterni mentre in domain le entità di dominio.

Anche  l’azienda dove lavoro ha diversi servizi che potrebbero essere utilizzati per costruire server MCP, portando funzionalità “core” dell’azienda direttamente in un LLM, oppure integrando un agente nelle sue soluzioni. L’idea è piaciuta ma poi, addentrandomi nello specifico dei progetti le lacrime hanno incominciato a scendere

Architettura inesistente

Logica di business dispersa tra database, controller, classi ed API Rest

Logica duplicata

Implementazioni disorganizzate

Cosa significa implementare un server MCP qui? Quale logica di business dovrei utilizzare? Quanto tempo mi serve per centralizzare la logica in un progetto con un’architettura degna di essere inserita nel famoso “libro dei mostri” di Harry Potter?

Primo grande insegnamento: l’architettura è fondamentale, conta e ripaga di ogni singolo minuto speso in più anziché scegliere la via più “facile” e “breve”.

La nuova architettura

Da qui in avanti prenderemo in considerazione il progetto ad API costruito con la clean architecture, aggiungere un server MCP significherà arrivare a questo modello:

Implementare le funzionalità MCP all’interno del progetto API è da considerarsi sbagliato sotto molti punti di vista.

Prima di tutto bisogna puntare alla separazione delle responsabilità, ogni progetto deve fare una cosa fatta bene, ecco quindi che uno si occuperà di API e uno di MCP.

In secondo luogo, se pensiamo ad un contesto di produzione devo poter scalare in modo indipendente le due interfacce o addirittura vorrei poter pubblicare solo le API e non il server MCP. La scelta è quindi quella di avere un progetto dedicato per ogni ambito, in grado di crescere e di dipendere solamente da quello che gli serve.

Configurazione

Il mio progetto era scritto in .NET e avevo una configurazione centralizzata nel progetto API. In .NET il file di configurazione (appsettings.json) viene caricato dal progetto API, che si occupa anche di orchestrare la dependency injection dei servizi. Dopo aver aggiunto alla solution il progetto MCP bisogna quindi pensare a come centralizzare la configurazione: duplicarlo è fuori discussione in quanto non è una best practice.

In questo progetto ho scelto di aggiungere un nuovo progetto condiviso alla soluzione, per centralizzare e condividere alcune risorse, tra cui gli appsettings

Progetto
IntegrationProject/
├── IntegrationProject.API/    
├── IntegrationProject.MCP/ 
├── IntegrationProject.Application/          
├── IntegrationProject.Domain/              
├── IntegrationProject.Infrastructure/  
├── IntegrationProject.Shared/ # <--- Shared configurations
└── IntegrationProject.Test/     

Il file di configurazione va anche suddiviso: non tutte le configurazioni si riferiscono agli elementi core del progetto, alcuni si riferiscono alle api, mentre altre configurazioni saranno dedicate al progetto MCP. Ci ritroveremo quindi con due appsettings, uno centralizzato e uno dedicato al progetto. Se un domani nascerà un terzo progetto, per esempio GraphQL, anche lui avrà un proprio appsettings base e quello condiviso tra tutti.

Come facciamo a caricare il file di configurazione? In un contesto di sviluppo andremo a caricare entrambi i file di appsettings:

Progetto
// Build path of shared folder
var sharedFolder = Path.Combine(Directory.GetCurrentDirectory(), "..", "IntegrationProject.Shared");
    builder.Configuration
    
#if DEBUG
        //Shared settings as first
        .AddJsonFile(Path.Combine(sharedFolder, "Configurations\\appsettings.Shared.Development.json"), optional: false, reloadOnChange: true)

        //Override with API specific settings
        .AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: true)

#elif RELEASE
    //[...]
#endif
;

In un contesto di produzione andremo invece a valorizzare il percorso di un file di configurazione dedicato, passato come parametro al nostro container con una variabile di ambiente.

Progetto
#if RELEASE
  if(string.IsNullOrEmpty(appsettingPath)) {
    throw new ArgumentException("Appsettings path missing! you must provide it with --settingsPath argument");
  }
#endif

#if DEBUG
    //[...]
#elif RELEASE
    .AddJsonFile(appsettingPath, optional: false, reloadOnChange: true)
#endif
;

Secrets

Il progetto in questione, per gestire le chiavi e le informazioni sensibili, utilizzava dei secret gestiti da Visual Studio durante la fase di sviluppo e delle variabili d’ambiente in produzione.

In questo specifico caso ho dovuto duplicare i secrets, in quanto sono gestiti a livello di progetto e non di solution. Se fosse stato un progetto aziendale non lo avrei gestito in questo modo, avrei probabilmente utilizzato un servizio di KeyVault

Docker

Prima di implementare il progetto MCP, le API rest erano pubblicate come container docker e l’immagine era caricata sul mio Docker hub.

Cosa cambierà con il server MCP? Non molto, anche il nuovo progetto avrà il suo Dockerfile dedicato per costruirsi l’immagine.

Per questo progetto ho creato 3 docker compose differenti:

  • Uno per avviare entrambi i progetti (MCP + API)
  • Uno per avviare solo le API
  • Uno per avviare solo il server MCP
Progetto
IntegrationProject/
├── IntegrationProject.API/    
├──── DockerFile             # Build API container        
├──── docker-compose.yml     # Run only api container
├── IntegrationProject.MCP/ 
├──── DockerFile             # Build MCP server container        
├──── docker-compose.yml     # Run only api container             
├── IntegrationProject.Application/          
├── IntegrationProject.Domain/              
├── IntegrationProject.Infrastructure/  
├── IntegrationProject.Shared/    
├── IntegrationProject.Test/     
└── docker-compose.yml       # Run API + MCP server

L’unica attenzione che dobbiamo mantenere è di aggiungere i parametri “stdin_open: true” e “tty: false” nel docker compose, riferiti al container del server MCP poiché sono necessari per comunicare con il client MCP.

docker-compose.yml
services:
  the-integration-project-mcp:
    image: <image-name-mcp>:latest
    # [...]
    stdin_open: true 
    tty: false
    # [...]

Cache

In uno scenario enterprise l’architettura di questo progetto si sarebbe evoluta ulteriormente, diventando un sistema a microservizi: uno dedicato alle API Rest, uno dedicato al server MCP e uno con la logica di business per ogni ambito di dominio.

Tutto questo era però un overkill per il mio servizio API che doveva girare sul mio Homelab, ma non mi piaceva che entrambi i sistemi fossero totalmente indipendenti senza la condivisione di nessuna risorsa. Il servizio API già utilizzava HybridCache (leggi qui il mio articolo dedicato se non lo hai fatto!) per memorizzare alcune informazioni e quindi ho pensato: perché non aggiungere una cache di secondo livello?

La libreria HybridCache ci permette di gestire una cache di primo livello (in memory) e opzionalmente una di secondo livello (per esempio Redis). Ho quindi aggiunto al docker compose l’istanza di un container con Redis, in modo tale da centralizzare le cache dei due servizi ed evitare che entrambi vadano a leggere le stesse informazioni dalla source più volte del necessario.

Nel docker compose (quello presente nella root del progetto, riferito all’avvio del server MCP ed API insieme) ho aggiunto Redis e una rete dedicata, per far parlare tra di loro le varie istanze dei continer:

docker-compose.yml
name: the-integration-project
services:
  ###################################
  # Rest API
  ###################################
  the-integration-project-api:
    image: <image-name-api>:latest
    # [...]
    depends_on:
      - redis
    networks:
      - the-integration-project-net

  ###################################
  # MCP Server
  ###################################
  the-integration-project-mcp:
    image: <image-name-mcp>:latest
    # [...]
    stdin_open: true 
    tty: false
    depends_on:
      - redis
    networks:
      - the-integration-project-net

  ###################################
  # Shared cache
  ###################################
  redis:
    image: "redis:alpine"
    # [...]
    networks:
          - the-integration-project-net

###################################
# Network
###################################
networks:
  the-integration-project-net:
    driver: bridge

Versioning

Ad ogni nuova implementazione sviluppata ed integrata nel ramo master del repository, una pipeline di DevOps si occupa di creare l’immagine docker con una nuova versione, per poi caricarla sul mio docker hub.

Integrazioni

Come detto nell’introduzione, i server MCP dialogano con i client tramite il protocollo JSON-RPC 2.0 e questo può avvenire anche se la soluzione è in un container. Docker si occuperà di avviare la soluzione mentre strumenti come n8n o Claude Desktop si collegheranno e inizieranno a consumare tool, risorse o altro.

Pubblicherò nei prossimi giorni un articolo dedicato su come integrare un server MCP in un container Docker con Claude Desktop

Tempi di sviluppo

Il progetto che ho migrato esponeva una decina di API REST e in 3/4 ore di lavoro ho:

  • Creato il nuovo progetto MCP
  • Scritto i tool con le descrizioni dei metodi e dei parametri
  • Spezzato gli appsettings tra quelli condivisi e quelli specifici per il progetto
  • Aggiornato la documentazione
  • Creati i dockerfile mancanti e i docker-compose dedicati

Quanto ci avrei impiegato per fare tutto questo in un progetto con gli stessi endpoint, ma senza una struttura chiara e la logica di business sparsa e disordinata? Lascio a voi la risposta.

Conclusioni

Aggiungere funzionalità MCP ai nostri progetti diventerà sempre più necessario con l’avanzata dell’AI (che ormai permea molti strumenti e dispositivi che utilizziamo quotidianamente). Il progetto di esempio presentato in questo articolo lo utilizzo a casa, collegato ad un modello LLM: in questo modo ho esteso le capacità del modello con delle funzioni specifiche che diversamente avrei dovuto gestire manualmente. Ora sono in grado di dialogare con il mio “Jarvis” chiedendogli di fare azioni specifiche (es. attivare l’irrigazione, interagire con le luci di casa, ecc)

L’architettura del progetto diventa quindi fondamentale, aver scritto un’applicazione “in velocità”, senza regole e con un’architettura errata presenterà il conto un domani, quando dovremo estendere le funzionalità e far evolvere il nostro prodotto.

Badate bene, questo discorso non si applica solo a questo contesto, è ovviamente più generale: come sviluppatori noi creiamo dei prodotti a “scatola chiusa”, l’utente finale non sarà mai grado di vedere le righe di codice o di apprezzare il come le abbiamo scritte. Sviluppare in modo corretto e longevo permetterà però al nostro prodotto di adattarsi ad un mondo in continua evoluzione nel minor tempo possibile.

Grazie per aver letto, alla prossima!

Condividi questo articolo
Shareable URL
Post precedente

Come integrare Sonarqube in Gitlab

Prosimo post

Cache frontend: localStorage, sessionStorage e IndexedDB

Leggi il prossimo articolo