code-bridge
by ITSWProj
README.md
# claude-dispatch
Server MCP che espone Claude Code a Claude Desktop, per lavorare su un progetto senza copiare e incollare prompt tra le due applicazioni.
Si ragiona su un problema nella chat di Claude Desktop, e da lì si delega l'esecuzione a Claude Code sul progetto reale. I lavori lunghi girano in background: si avviano, si continua a fare altro, si recupera il risultato quando è pronto.
## ⚠️ Prima di installarlo
**Questo server permette a un modello di eseguire Claude Code sulla tua macchina, con accesso al filesystem.** È una superficie di rischio reale e va capita prima di usarlo.
Cosa fa e cosa non fa:
- **Gira solo in locale.** Il trasporto è stdio: Claude Desktop lancia il server come processo figlio e ci parla via stdin/stdout. Nessuna porta in ascolto, nessun endpoint pubblico, niente che entri dal firewall.
- **Non limita cosa Claude Code può fare.** Il server passa il prompt e basta. I veri freni sono i permessi in `.claude/settings.json` del progetto: se lì è consentita la scrittura, l'agente scrive.
- **Non c'è allowlist di directory.** Qualunque percorso esistente passato come `cwd` viene accettato. Se ti serve un vincolo più stretto, aggiungi il controllo in `avviaClaude` ed `eseguiClaude`.
- **Ogni chiamata a strumento passa dalla conferma di Claude Desktop.** Il client chiede l'approvazione prima di invocare il server — a meno che non l'abbia disattivata.
**La raccomandazione:** configura permessi restrittivi nei progetti su cui lo usi, in particolare per le operazioni irreversibili (migration, cancellazioni, deploy). Un agente che deve chiedere prima di fare danni è più utile di uno veloce.
## Requisiti
- Windows, Linux o macOS — testato su Windows 10/11 e Debian/Kali
- Node.js 18+ — testato su 22
- [Claude Code](https://claude.com/claude-code) installato e autenticato
- Claude Desktop
## Installazione
```bash
git clone https://github.com/ITSWProj/claude-dispatch.git
cd claude-dispatch
npm install
```
Verifica che parta:
```bash
node server.js
```
Deve restare appeso in silenzio, senza stampare nulla: sta aspettando input su stdin. È il comportamento corretto. Esci con `Ctrl+C`.
## Configurazione
Il file di configurazione di Claude Desktop è:
| Sistema | Percorso |
|---|---|
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
**Windows:**
```json
{
"mcpServers": {
"claude-dispatch": {
"command": "C:\\percorso\\a\\node.exe",
"args": ["C:\\percorso\\a\\claude-dispatch\\server.js"]
}
}
}
```
**macOS / Linux:**
```json
{
"mcpServers": {
"claude-dispatch": {
"command": "/percorso/assoluto/a/node",
"args": ["/percorso/assoluto/a/claude-dispatch/server.js"]
}
}
}
```
Tre punti dove si sbaglia facilmente:
- **Percorso assoluto all'eseguibile di Node**, non la parola `node`. Claude Desktop non eredita il PATH della shell. Se usi nvm, punta all'eseguibile della versione specifica (`C:\nvm4w\v22.20.0\node.exe` su Windows, `~/.nvm/versions/node/v22.20.0/bin/node` su Unix), non alla junction o al symlink `current`, che cambiano a ogni `nvm use`. Per trovarlo: `where node` su Windows, `which node` su Unix.
- **Su Windows, backslash raddoppiati** nel JSON, oppure slash normali (`C:/percorso/...`), che Node accetta anche lì.
- **Percorso assoluto anche in `args`**: il processo viene avviato con una working directory indefinita, quindi `./server.js` non risolve. Su Unix nemmeno `~` viene espanso — quella è una funzione della shell, e qui la shell non c'è.
Poi chiudi Claude Desktop completamente — su Windows anche dall'area di notifica, su macOS con `Cmd+Q` e non con la X — e riavvialo. La configurazione si legge solo all'avvio.
Se non compare nulla, il sospetto numero uno è il JSON: una virgola di troppo disattiva silenziosamente *tutti* i server, senza messaggi. Passalo da un validatore.
Lo stderr del server (dove finiscono i `console.error()`) è nel file `mcp-server-claude-dispatch.log`, nella cartella `logs/` accanto al file di configurazione. Se il processo muore all'avvio, il motivo è scritto lì.
## Strumenti
| Strumento | Uso |
|---|---|
| `claude_run` | Esegue un prompt e attende la risposta. Per compiti brevi. |
| `claude_start` | Avvia un lavoro in background, ritorna subito un `job_id`. |
| `claude_wait` | Attende un job avviato; se non è pronto entro `hold_ms` (default 30s, massimo 60s) riporta "in corso". |
| `claude_status` | Elenca le istanze attive, diagnostica quelle bloccate, mostra la credenziale in uso. |
| `claude_kill` | Termina un job e tutti i suoi processi figli. |
| `claude_kill_all` | Pulizia di emergenza: termina tutte le istanze. Richiede conferma esplicita. |
### Quale usare
Il discriminante è il tempo. Una domanda circoscritta — "leggi questi file e spiegami come funziona X" — sta bene in `claude_run`. Un lavoro vero — refactoring, implementazione, esplorazione di un'area sconosciuta — va con `claude_start`, poi `claude_wait` finché non è pronto.
`claude_run` ha un timeout (default 120s) perché una chiamata sincrona che non ritorna blocca la conversazione. `claude_start` non ne ha: il tetto sta sull'*attesa*, non sul lavoro.
Anche quell'attesa però è limitata: `hold_ms` viene tagliato a 60 secondi. Passare valori più alti non allunga l'attesa — semplicemente si richiama `claude_wait` con lo stesso `job_id` finché il lavoro non è concluso. È un vincolo voluto: `hold_ms` lo sceglie il modello, e senza tetto una singola chiamata potrebbe bloccare la conversazione per minuti.
## Il limite di istanze contemporanee
Ogni istanza di Claude Code è un processo Node con un contesto in memoria: costa centinaia di MB. Il modello, se una chiamata gli sembra fallita, tende a **riprovare** — e senza un freno ogni tentativo apre un'istanza nuova. Quattro o cinque tentativi bastano a esaurire la RAM e a congelare la macchina.
Da qui una difesa a tre strati, dal più affidabile al meno:
1. **Tetto rigido.** `MAX_CONCORRENTI` istanze al massimo, sommando `claude_start` e `claude_run`, più una soglia minima di RAM libera. È deterministico: non dipende da nessuna decisione del modello né dell'utente, ed è l'unico strato che regge anche durante un loop di retry.
2. **Guard per progetto.** Se sullo stesso progetto c'è già un lavoro vivo, non se ne avvia un secondo: lo strumento restituisce `DECISIONE RICHIESTA` e la scelta torna all'utente.
3. **Le `description` degli strumenti**, che dicono al modello di guardare `claude_status` prima di avviare e di non ritentare dopo un rifiuto. È persuasione, non applicazione — riduce la frequenza con cui i primi due strati devono intervenire, ma non ci si fa affidamento da sola.
I due valori stanno in cima a `server.js`:
```js
const MAX_CONCORRENTI = 2; // istanze contemporanee, start + run
const SOGLIA_RAM_MB = 2000; // RAM libera sotto cui non si avvia più nulla
```
Il default è deliberatamente basso. Il costo di tenerlo basso è un'attesa; il costo di tenerlo alto è una macchina congelata. Se ti servono davvero tre lavori in parallelo, cambia la costante.
### Le due domande, che sono diverse
Quando `claude_start` trova un conflitto restituisce `DECISIONE RICHIESTA` con delle opzioni, e la chiamata va ripetuta con il parametro `mode` scelto. I casi sono due e vale la pena non confonderli.
**C'è un lavoro vivo sul progetto.** Le opzioni sono `wait` (attendere quel `job_id`) o `restart` (terminarlo e ripartire). Non esiste una terza via: un processo headless ha già lo stdin chiuso, quindi *non c'è modo* di infilargli dentro un secondo prompt. O si aspetta, o si interrompe.
**Non c'è nessun processo, ma esiste una sessione precedente sul disco.** Le opzioni sono `resume` (riprendere quel contesto) o `fresh` (ripartire pulito). Qui non si sta riusando un processo — non ce n'è nessuno — ma il contesto salvato da Claude Code, tramite `--resume`.
Passando `mode` fin dalla prima chiamata, il guard non chiede nulla e si va dritti.
### Il `session_id` conta
Ogni risposta restituisce un `session_id`. Riprendere una sessione fa ripartire il lavoro con il contesto già caricato, invece di riesplorare il progetto da zero.
Non è un dettaglio: la prima invocazione su un progetto di medie dimensioni può costare qualche decina di centesimi. Riusare la sessione riduce il costo dei giri successivi di un ordine di grandezza.
Dalla 0.3.0 il server **tiene traccia da solo dell'ultima sessione riuscita per ogni progetto** e la riprende in automatico. Non serve più ripassare il `session_id` a mano da una chat all'altra, anche se resta possibile. Per forzare una sessione nuova: `mode: "fresh"` su `claude_start`, `fresh: true` su `claude_run`.
La mappa è indicizzata per progetto, con il percorso normalizzato (`path.resolve`, più lowercase su Windows). Senza quella normalizzazione `C:\progetti\app` e `C:/progetti/app/` sarebbero due progetti distinti, e né il resume né il guard scatterebbero.
Da non confondere con il token di autenticazione: il `session_id` identifica una conversazione con Claude Code, il token identifica te. Vedi [Autenticazione](#autenticazione).
## Autenticazione
Il server non sceglie come autenticarti: si limita a osservare quale credenziale è presente. Ne supporta due, e la differenza non è solo di comodità.
### Login salvato
Il caso normale: hai eseguito `claude` da terminale e completato l'accesso. Il token OAuth ha una scadenza, e la prima invocazione dopo una pausa lunga lo rinnova riscrivendo il file di credenziali. È osservabile: quel file resta intoccato per un'intera giornata di lavoro e cambia solo al primo comando del mattino.
Quella finestra di rinnovo è il punto debole. **Lanciando in parallelo, il dispatch può mandare due processi a freddo sullo stesso rinnovo**: uno ruota il token, l'altro resta con quello vecchio. Un utente al terminale apre una sessione per volta e non incontra mai questa corsa.
Da qui il **lock di riscaldamento**: se non gira niente da mezz'ora, la prima invocazione prende un lock e le altre aspettano. Il rilascio avviene alla fine del lavoro o dopo 45 secondi, quello che viene prima — un lavoro lungo non deve tenere fermi gli altri — con un salvagente che apre comunque il lock, perché un lock senza scadenza è un deadlock in attesa di succedere.
### Token a vita lunga
Consigliato per questo server, perché **elimina il problema invece di gestirlo**: chi non usa il login salvato non attraversa mai il rinnovo automatico, e il lock di riscaldamento si disattiva da solo.
```bash
claude setup-token
```
Poi nel `claude_desktop_config.json`, accanto a `command` e `args`:
```json
"env": { "CLAUDE_CODE_OAUTH_TOKEN": "il-token" }
```
Il campo `env` non è opzionale: **una variabile esportata nella shell non arriva qui**. Claude Desktop non lancia il server da una shell, quindi né lui né i suoi figli la ereditano — è lo stesso meccanismo per cui serve il percorso assoluto a Node. Vale identico per `ANTHROPIC_API_KEY`, che il server riconosce allo stesso modo.
### La trappola della precedenza
Le credenziali d'ambiente prevalgono sul login salvato. Quindi se `CLAUDE_CODE_OAUTH_TOKEN` o `ANTHROPIC_API_KEY` sono nel campo `env` e vengono rifiutate, **rifare il login non serve a niente**: il login salvato non viene nemmeno consultato. Si rifà l'accesso, si rivede lo stesso errore e non si capisce perché.
Per questo il server rileva quale credenziale è in uso e adatta il messaggio: con un token in ambiente dice di rigenerare quello, non di fare il login.
Il rilevamento resta un'inferenza dall'esterno — il server vede il proprio `process.env` e l'esistenza del file di credenziali, niente di più. Su macOS il login può stare nel portachiavi senza che il file esista, quindi l'assenza del file è etichettata `incerto` e non "nessun login". La risposta autorevole la dà `/status` dentro una sessione interattiva di `claude`.
Il modo rilevato compare in `claude_status` e in una riga di diagnostica su stderr all'avvio, nel file di log accanto alla configurazione. Se il server non parte, è la prima cosa da leggere.
### Quando gli avvii si bloccano
Un errore di autenticazione o di quota, riportato come fallimento generico, somiglia a un guasto passeggero: il modello riprova, e ogni tentativo è un processo che costa secondi e memoria per fallire in modo identico.
Il server riconosce questi fallimenti e apre un **circuito** che fa fallire i successivi in un millisecondo, con un messaggio che dice cosa fare. Il riconoscimento usa i codici strutturati di Claude Code quando ci sono (`authentication_failed`, `oauth_org_not_allowed`) e altrimenti le formulazioni note dei messaggi.
Il circuito si riapre sempre, a velocità diverse: un minuto per i fallimenti che un comando esterno può risolvere, cinque per quelli che richiedono un intervento e per i limiti di quota.
**Perché anche i casi apparentemente definitivi si riaprono**: la corsa sul rinnovo descritta sopra non ha un messaggio suo, e può presentarsi con le stesse parole di un login davvero scaduto. Un blocco fino all'intervento manuale punirebbe una condizione che si sarebbe risolta da sola. Il compromesso è concedere un tentativo di verifica ogni cinque minuti: se il problema è reale quel tentativo fallisce e richiude il circuito, al costo di un processo ogni cinque minuti; se era spurio, il servizio riprende da solo.
Per riabilitare subito, quando sai di aver risolto:
```
claude_status(riprova_auth: true)
```
### Diagnosticare un lavoro che sembra bloccato
A un processo headless non si può chiedere "sei ancora attivo?". Non è un demone, non ha un socket di controllo, e il suo stdin è già stato chiuso passandogli il prompt: non esiste un canale per interrogarlo.
`claude_status` aggira il problema incrociando tre segnali indiretti:
- **il processo esiste** (`kill(pid, 0)`, che non uccide: è una sonda);
- **il tempo CPU si muove**, misurato su due campioni a distanza di poco meno di un secondo;
- **il file di sessione cresce** — Claude Code appende ogni turno a un `.jsonl` sotto `~/.claude/projects/`, quindi il suo `mtime` dice quando ha prodotto l'ultimo turno.
Nessuno dei tre basta da solo. Insieme distinguono i tre casi che contano: morto, piantato, o semplicemente lento dentro un'operazione lunga. Con `dettaglio: true` lo strumento riporta anche RAM occupata e secondi dall'ultimo turno.
`claude_status` mostra due liste separate: **le istanze avviate da questo server**, su cui può agire, e **le altre presenti nel sistema** — lanciate a mano in un terminale, o rimaste orfane da un riavvio di Claude Desktop. Queste ultime sono solo informative: la RAM la consumano esattamente come le altre, ma un processo che il server non ha generato non viene mai terminato di iniziativa. Per farlo serve `claude_kill_all` con `include_altri: true`, che è esplicito e va chiesto.
## Limiti noti
- **Lo stato vive in memoria.** Riavviando Claude Desktop mentre un lavoro gira, il processo continua ma il `job_id` diventa irraggiungibile. Da questa versione quei processi restano almeno *visibili*: compaiono in `claude_status` tra le istanze esterne, e `claude_kill_all` li chiude. Un registro persistito su disco richiederebbe di confermare l'identità del pid contro il riuso, e non c'è ancora.
- **I job in background sopravvivono alla chiusura del client.** Su tutte le piattaforme: se hai chiuso Claude Desktop con lavori in corso, quei processi restano vivi. Vanno terminati dal `claude_kill_all` del server ripartito, oppure a mano (Gestione attività su Windows, `pkill -f "claude -p"` su Unix).
- **`restart` interrompe un lavoro a metà.** Non è un annullamento pulito: l'agente può aver già scritto parte dei file previsti. Se il progetto è sotto git, conviene passare da `git status` subito dopo.
- **Il timeout di `claude_run` uccide l'intero albero di processi**, agente incluso, e il lavoro parziale non è recuperabile. Se il compito potrebbe essere lungo, usa `claude_start`: lì il tempo non è un vincolo.
- **La prima invocazione su un progetto è la più lenta.** Sembra un blocco, non lo è.
- **L'identificazione delle istanze esterne è approssimativa.** Si cerca la stringa `claude` nella command line dei processi, quindi è un'euristica. Serve a mostrarle, mai a ucciderle senza conferma.
- **Il riconoscimento dei fallimenti di autenticazione dipende dalle formulazioni dei messaggi.** I codici strutturati sono stabili, il testo no: se una versione di Claude Code cambia il fraseggio, un errore può sfuggire al circuito e tornare a essere ritentato. Nessun falso blocco, però — la classificazione gira solo sui rami di fallimento, mai su un esito riuscito che parla di autenticazione.
- **Il rilevamento della credenziale è un'inferenza.** Il server legge il proprio `process.env` e verifica l'esistenza del file di login, niente di più. Su macOS un login nel portachiavi non produce quel file.
- **Su Windows le misure di RAM e CPU sono poco significative.** Passando da `shell: true`, il pid che il server conosce è quello di `cmd.exe`, non dell'agente: va bene per sapere se il lavoro è vivo e per terminarlo, meno per pesarlo. Su Unix il pid è direttamente `claude` e i numeri sono reali.
- **Nessun limite di spesa.** Il server non impone tetti di costo o di turni. Se servono, si passano a Claude Code con `--max-turns`.
## Come funziona
Un server MCP stdio non è un servizio in ascolto: è un processo figlio. Claude Desktop lo lancia e ci scambia messaggi JSON-RPC via stdin/stdout.
Da qui discende la regola più importante per chi mette mano al codice: **mai `console.log()`**. Ogni byte su stdout finisce nel canale del protocollo e corrompe il messaggio. Per il debug si usa `console.error()`, che scrive su stderr.
L'asincronia è gestita con `Promise.race` tra la fine del lavoro e un timer: nessun polling, nessuna euristica sul "sembra fermo". I job vivono in una `Map` con pulizia opportunistica dei conclusi da oltre dieci minuti.
Il prompt viaggia su **stdin**, non come argomento della riga di comando: è testo lungo e pieno di caratteri speciali, e qualunque escaping per la shell sarebbe fragile.
I processi di `claude_run` hanno un registro proprio, separato da quello dei job. Non serve a riprenderli — `claude_run` attende il risultato dentro la propria chiamata — ma a farli *esistere* agli occhi del tetto di concorrenza: fino alla 0.2.0 erano istanze vive e completamente invisibili al server, cioè metà dei processi generati non veniva contata.
### Le differenze di piattaforma
Il file è unico: le differenze sono decise a runtime da `process.platform` e concentrate in poche costanti, in `uccidiAlbero()` e nelle funzioni che ispezionano i processi.
Il nodo è che il processo restituito da `spawn()` non è mai l'agente, ma il primo anello di una catena — `cmd.exe → claude.cmd → node → agente` su Windows, `claude → node → agente` su Unix. Terminare solo quello lascerebbe l'agente vivo a consumare token senza più essere raggiungibile.
Su Windows serve `shell: true` perché `claude.cmd` è uno script batch che solo cmd.exe sa interpretare, e da lì `taskkill /T /F` per abbattere l'albero. Su Unix la shell non serve — un guadagno anche di sicurezza — e si usa `detached: true`, che rende il figlio capostipite di un process group: da quel momento `process.kill(-pid)` raggiunge l'intero gruppo con un solo segnale. Le due opzioni vanno insieme: senza `detached`, il PID negativo colpirebbe il gruppo del server stesso.
Per l'ispezione dei processi si usa `ps` su Unix e PowerShell su Windows — non `tasklist`, che non espone il tempo CPU, né `wmic`, deprecato.
Il codice è commentato in dettaglio, con il *perché* di ogni scelta non ovvia.
Entrambi i rami sono stati verificati: su Unix con `ps -eo pid,pgid` (il figlio deve avere PGID uguale al proprio PID, cioè essere capostipite del suo gruppo) e controllando che il kill abbatta l'albero senza toccare il server. macOS non è stato provato direttamente, ma usa lo stesso ramo.
## Licenza
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues