Questo articolo fa parte della serie «Un repo per i miei articoli». Riguarda il server MCP di una wiki LLM: una wiki di concetti tecnici in Markdown che un agente come Claude Code scrive, collega e interroga, con gli articoli pubblicati come fonti. Per farlo gli serve un'interfaccia con regole precise su cosa può leggere e cosa può scrivere, e per questo ho scritto wiki-mcp-api, un server MCP in TypeScript costruito su @mastra/mcp. Racconto come è fatto e perché lo stesso server si usa in due modi: in locale per modificare la wiki, via HTTP solo per servirla ad altri.
Due cartelle con ruoli diversi
Il server non contiene nessuna wiki. Legge due cartelle indicate dall'ambiente: WIKI_PATH, con le pagine, che si possono scrivere, e RAW_PATH, con le fonti da cui nascono le pagine, in sola lettura. Una terza variabile, WIKI_DOMAIN, è una riga che descrive di cosa parla quella wiki e finisce nelle istruzioni inviate al client. Con la stessa installazione posso così servire due wiki diverse, ciascuna con la sua configurazione.
Il confine tra le due cartelle sta nel codice. writePage risolve il percorso, controlla che resti dentro WIKI_PATH e accetta solo file .md. Le letture delle fonti passano da tool separati, che rifiutano percorsi assoluti e ...
I tool
In stdio i tool sono undici:
| Tool | A cosa serve |
|---|---|
wikiSearch, wikiListPages, wikiReadPage
|
cercare, elencare e leggere le pagine |
wikiGraph, wikiStatus
|
grafo dei link e conteggi per tipo e stato |
wikiWritePage, wikiAppendLog
|
scrivere una pagina, aggiungere una voce al registro |
wikiListRaw, wikiReadRaw
|
elencare e leggere le fonti |
wikiChecksum, wikiRawDiff
|
riconoscere una fonte già vista e vedere cosa è cambiato |
Scrivere senza rompere la wiki
wikiWritePage valida il frontmatter con zod prima di scrivere: tipo tra un elenco fisso, almeno un tag, fonti come lista, aggiornato nel formato YYYY-MM-DD e stato tra bozza, stabile e da-rivedere. Le quattro pagine di servizio (index, log, sources, overview) sono esenti. Se il frontmatter non va, l'errore dice quale campo e perché, e l'agente può correggere da solo.
Per le scritture concorrenti c'è un lock sul singolo file (proper-lockfile). Evita che due scritture si mescolino, ma non risolve un conflitto tra due autori che scrivono cose diverse.
La ricerca
wikiSearch ordina per rilevanza con un punteggio in stile TF-IDF, con pesi per campo (il titolo conta più dei tag e i tag più del contenuto) e senza normalizzazione sulla lunghezza della pagina. Restituisce pochi risultati, 10 con una query e 50 senza: i client MCP troncano le risposte lunghe per proteggere il contesto, e un taglio casuale rischia di perdere proprio le pagine più rilevanti. Nei risultati ci sono metadati e uno snippet, non l'intero contenuto.
Aggiornare una fonte che è cambiata
Le fonti sono file e cambiano: un articolo viene corretto, un documento ampliato. All'ingest, wikiChecksum calcola lo sha256 del file e il registro sources.md lo associa alle pagine che ne sono nate. Alla sessione successiva basta ricalcolare il checksum di ogni fonte: se è diverso da quello registrato, la fonte è cambiata. Il checksum individua quali fonti sono cambiate senza doverle rileggere tutte, e il confronto è sul contenuto e non sul nome, quindi una fonte rinominata non risulta nuova.
Dice però solo che qualcosa è cambiato, non cosa. Se all'agente basta rileggere il file aggiornato per decidere cosa modificare nella wiki, spesso non si accorge delle differenze, soprattutto con file grandi, oppure le sottovaluta e non le riporta nelle pagine. Per questo wikiRawDiff confronta la versione vecchia del file, quella che aveva il checksum registrato e che ritrova nella storia git di raw/, con quella attuale, e restituisce le differenze. L'agente valuta solo quelle e aggiorna le pagine che le riguardano. Quando un diff di testo non c'è, ripiega sul confronto per checksum e sulla lettura della fonte. Serve quindi che raw/ stia in un repo git.
stdio e HTTP
Il server ha due ingressi con scopi diversi. In stdio, lanciato dal client sulla stessa macchina, si usa per lavorare sulla wiki: espone tutti i tool, scrittura compresa. L'agente scrive le pagine come modifiche non committate del repo della wiki, che si rivedono e si committano a mano. Così ci sono le due garanzie che il server non dà da solo: l'attribuzione, perché il commit è di chi esegue l'agente, e un controllo umano, perché il diff si rivede prima di committare.
In HTTP il server serve la conoscenza ad altri, quindi è di sola lettura. Richiede un token Bearer (senza MCP_HTTP_TOKEN il processo non parte) e non gestisce TLS, che va messo a monte con un reverse proxy. Un client remoto non ha né attribuzione né controllo umano, e il lock evita solo la corruzione concorrente.
Per lo stesso motivo da remoto si servono di default solo le pagine, cioè la conoscenza già elaborata. I tool sulle fonti (wikiListRaw, wikiReadRaw, wikiRawDiff, wikiChecksum) restano fuori, perché raw/ può contenere materiale non pensato per altri, come bozze. Si riattivano con MCP_HTTP_EXPOSE_RAW=true, valore esatto, e all'avvio il server dice se le fonti sono esposte. I test coprono la differenza tra i due ingressi: per controllare che servissero li ho lanciati senza la modifica e falliscono.
Possibili miglioramenti
- I valori di
tiposono fissati nel codice:concetto,soggetto,procedura,normativa,entitaeanalisi. Per una wiki con altre categorie andrebbero resi configurabili. - La ricerca non ha un indice: a ogni query il server controlla tutti i file e ricalcola il punteggio su tutte le pagine, e la cache evita solo di rileggere e riparsare i file non modificati. Non l'ho misurato su wiki grandi, dove un indice in memoria potrebbe servire.
- L'accesso HTTP ha un solo token condiviso e niente TLS.
- Il server stdio non ha una modalità di sola lettura: i tool di scrittura ci sono sempre.
-
wikiRawDiffrichiedegitsulla macchina che lancia il server, e per i PDF anchepdftotext.
Il progetto ha una CI su GitHub Actions che lancia lint e test a ogni push su main e a ogni pull request. Come ho reso pubblico lo strumento della serie, controllando i segreti prima del push, è raccontato in Rendere pubblico un repo: cosa ho controllato prima del primo push.
Top comments (0)