MCP editoriale controllato
A cosa serve
Il server MCP editoriale consente a un client interattivo autorizzato di consultare il corpus e applicare un insieme stretto di azioni sul workflow Issue. Non espone query o route generiche né accesso diretto a D1.
flowchart LR
C["ChatGPT o MCP Inspector"] -->|"OAuth e Streamable HTTP"| A["Cloudflare Access"]
A -->|"assertion umana"| M["Worker MCP editoriale"]
M -->|"fetch pubblico same-zone<br/>service identity dedicata"| AA["Cloudflare Access API"]
AA -->|"route e metodi allowlisted"| API["API Worker"]
API -->|"ventidue permessi stretti"| D1[("D1 editoriale")]
M -. "nessun binding" .-> X["D1 · Queue · Mailjet · provider AI"]
Componenti che lo usano
- un client MCP remoto compatibile con OAuth e Streamable HTTP;
- Cloudflare Access come confine dell'identità umana;
- il Worker dedicato
apps/mcp; - l'API Worker come unica autorità di accesso e mutazione editoriale;
- D1, raggiungibile soltanto attraverso l'API.
Il Worker crea un server MCP stateless per richiesta. Non usa Durable Object, session storage, D1, Queue, R2, KV o binding provider.
La configurazione Wrangler autorevole dichiara insieme nodejs_compat e
global_fetch_strictly_public. Anche se Worker MCP e API appartengono alla
stessa zona Cloudflare, il fetch() verso il custom domain API torna quindi al
front door pubblico, attraversa Cloudflare Access Service Auth e raggiunge
l'API Worker. Il flag conserva il canale pubblico autenticato esistente: non è
un nuovo accesso e non autorizza un Service Binding.
Dati e artefatti scambiati
Il client invia soltanto input chiusi e bounded. Il Worker restituisce proiezioni strutturate di Issue, Topic, Knowledge Asset e termini di glossario. Il testo editoriale, anche quando assomiglia a un'istruzione, resta un dato inerte: non influenza routing, metodo, host, header o selezione delle route.
I quarantuno tool effettivi espongono operazioni editoriali e email nominate:
| Tool | Input essenziale | Output |
|---|---|---|
get_editorial_overview |
oggetto vuoto | primo inventario bounded delle quattro risorse |
search_editorial_content |
risorsa, query, filtri ammessi, limite e cursore | massimo 20 summary uniformi |
get_editorial_content |
risorsa e identificatore canonico | un record canonico proiettato |
patch_editorial_content |
risorsa, identificatore canonico e soli campi da cambiare | record completo risultante dopo merge, validazione dello schema canonico e persistenza, senza piano o gate di workflow |
plan_editorial_work / recommend_next_editorial_action |
outcome bounded, piano/stato corrente, oppure plan id | piano con snapshot deterministico oppure prossima azione conservativa |
apply_editorial_plan / get_editorial_work_status |
piano completo, planId+confirmedVersion, oppure work ID |
esecuzione riprendibile o stato degli step |
create_editorial_topic / update_editorial_topic |
Topic con idempotency key o patch con revisione attesa | Topic proposto con ID server-owned o aggiornato |
create_knowledge_asset / update_knowledge_asset |
Knowledge Asset con idempotency key o patch con revisione attesa | asset proposto con ID server-owned o aggiornato |
create_glossary_term / update_glossary_term |
termine con idempotency key o patch con revisione attesa | termine proposto con ID server-owned o aggiornato |
check_issue_generation_eligibility |
Topic | lifecycle, fonti, copertura e prossima azione |
request_issue_generation |
Topic | Issue esistente oppure job attivo/nuovo |
get_issue_generation_job |
UUID job | stato e prossima azione |
run_issue_quality_check |
slug Issue | diagnostica deterministica |
update_issue_draft |
contenuto, revisione attesa, motivo | draft canonico aggiornato |
approve_issue / reject_issue |
slug e nota opzionale | decisione esplicita senza pubblicazione |
publish_issue |
slug | pubblicazione esplicita senza email |
unpublish_issue |
slug e motivo bounded | ritorno esplicito a draft con output non pubblico sanitizzato |
get_email_contact_list_overview / lookup_email_contact_membership |
oggetto vuoto o un indirizzo validato | stato lista o membership sicura senza restituire PII |
subscribe_email_contact / unsubscribe_email_contact |
indirizzo e idempotency key | tentativo sanitizzato e stato membership |
get_email_campaign_overview / get_email_campaign / get_email_campaign_attempts |
pagina bounded o slug Issue | aggregate, stato, azioni e storia tentativi senza destinatari |
schedule_newsletter |
slug, listAlias, at, timezone, conferma e idempotency key | 202 con campaign e job; Workflow verso Mailjet |
get_email_campaign_job |
UUID job | stato bounded senza mutazione provider |
render_email_campaign_content |
slug Issue | subject, HTML e testo canonici senza chiamata provider |
abort_email_campaign |
campaign ID, conferma e motivo bounded | audit canonico di un workflow non schedulato; il draft provider resta conservato |
L'overview della lista separa la configurazione allowlisted posseduta
dall'applicazione dallo stato live posseduto da Mailjet. Con configurazione
valida, alias sicuro e capacità configurate restano visibili anche se il
provider è unavailable; in quel caso il conteggio live è ignoto,
availableActions è vuoto e la diagnostica contiene soltanto categoria e
retryability sanificate. schedule_newsletter valida sempre
l'alias esplicito contro l'allowlist locale e non usa list ID, default impliciti
o il successo dell'overview come fonte dell'alias. Un campaign job ambiguo
resta fail-closed nel Workflow; l'operatore interrompe con abort_email_campaign
e, se serve un nuovo invio, chiama di nuovo schedule_newsletter.
I normali create non ricevono identificatori dal client: D1 assegna
atomicamente ID con almeno tre cifre e prosegue con TOPIC-1000, KA-1000 e
TERM-1000 dopo 999, quando il piano viene applicato o quando un tool primitivo
riceve una idempotency key. Il numero editoriale
dell'Issue usa la sequenza server-owned già esistente al gate separato di
generazione. Gli identificatori accettati in lettura e aggiornamento sono slug
canonici per le Issue, gli ID canonici o lo slug del glossario. URL, traversal,
separatori di path e caratteri di controllo sono rifiutati.
Autenticazione e autorizzazione
Cloudflare Access autentica l'umano verso il dominio MCP. Il Worker verifica firma, issuer, audience, scadenza e tipo dell'identità; una service assertion non può sostituire l'utente umano.
Il token OAuth del client non viene inoltrato all'API. Il Worker crea
server-side gli header della propria service identity dedicata. La policy
applicativa dell'API riconosce quel client e lo limita esattamente a
admin:read, quattro permessi stretti per il workflow Issue, sei permessi
create/update specifici per risorsa, editorial:plan, editorial:apply e otto
permessi email distinti per
contacts read/manage e campaign read/prepare/content/schedule/deliver/reconcile.
Non riceve editorial:write, il broad permission legacy campaign:draft o
permessi deployment. Le credenziali dei due confini restano distinte.
Cosa possiede il provider
Cloudflare possiede disponibilità del runtime Worker, applicazione e policy Access, lifecycle OAuth, firma delle assertion, service token, custom domain e telemetria della piattaforma.
Cosa possiede Architecture Coffee
Il repository possiede tool, schemi, proiezioni, allowlist route/metodo, limiti, timeout, mapping degli errori, log sanitizzati e configurazioni Wrangler. L'API possiede autorizzazione applicativa e accesso a D1. D1 resta l'autorità del contenuto e dello stato editoriale runtime.
Il contratto applica:
- richiesta MCP massima di 128 KiB;
- risposta strutturata di un tool massima di 256 KiB;
- body upstream massimo di 1 MiB;
- timeout di 5 secondi per richiesta e 12 secondi per tool;
- al massimo un retry sicuro per GET, nessun retry per POST/PUT/PATCH e quattro richieste upstream per invocazione;
- massimo 100 record scansionati per risorsa nell'overview;
- massimo 20 risultati e 500 code point per summary.
Operazioni automatiche, manuali e protette
Test, typecheck, build, dry-run e controllo statico del bundle sono automatici e non distribuiscono nulla. Connessione Inspector, configurazione Access, creazione o rotazione dei secret, deployment development, connessione ChatGPT e scansione dei tool sono operazioni remote protette. Richiedono una Issue operativa e una finestra autorizzata separate.
La connessione production si presenta come Architecture Coffee Editorial
MCP, è invocabile con @aceMCP ed è l'autorità predefinita per le richieste
editoriali normali. La connessione development si presenta come Architecture
Coffee Editorial MCP - DEV, è invocabile con @aceMCP-dev e può essere
selezionata soltanto se Fra nomina DEV o development nella richiesta corrente.
Il nome e il tag production canonici non contengono PROD. Un errore o
un'indisponibilità production non autorizza fallback, retry o confronto su
development.
Il Worker espone il nome previsto tramite serverInfo.name; nome, descrizione
e tag della connessione ChatGPT usano lo stesso metadata canonico durante il
rollout operativo separato. Il rename non cambia endpoint, autenticazione,
autorizzazioni, binding o set di tool.
Il flusso conversazionale resta osservabile:
outcome → piano read-only → sole domande determinanti → conferma corrente
→ apply riprendibile → stato → eventuale generazione Issue separata
→ review → approvazione → pubblicazione → gate email separati
Accettazione, approvazione, rifiuto e pubblicazione sono operazioni protette distinte:
richiedono il comando corrente esplicito di Fra. Non esiste confirmed: true,
l'approvazione non pubblica e la pubblicazione non prepara né invia email.
Subscribe, unsubscribe, scheduling e delivery richiedono a loro volta comandi
correnti distinti. Nessun output di un tool autorizza il gate successivo.
Il runbook di verifica distingue la prova repository dalla futura attivazione development. Un merge non configura Access, non crea secret e non distribuisce il Worker.
Fallimenti e comportamento fail-closed
- configurazione incompleta o upstream non corrispondente all'ambiente:
/healthrestituiscenot_ready; - assertion assente, invalida, scaduta o revocata:
UNAUTHENTICATED; - identità non umana:
FORBIDDEN; - input, proprietà o identificatore non ammesso:
INVALID_ARGUMENT; - configurazione email server-side disabilitata, assente, invalida o ambigua:
uno dei codici canonici
EMAIL_CHANNEL_*,EMAIL_TARGET_*oEMAIL_PROVIDER_*, maiINVALID_ARGUMENT; - body MCP oltre limite: HTTP 413 con errore JSON-RPC bounded;
- redirect, status inatteso, JSON malformato o proiezione divergente:
UPSTREAM_CONTRACT_ERROR; - timeout o dipendenza indisponibile: errore sanitizzato e nessun fallback.
Non esiste fallback a D1, a una route Admin generica, a un altro host o a una service identity più privilegiata.
Verifiche operative
- discovery di esattamente trentaquattro tool con annotazioni coerenti;
- rifiuto delle proprietà aggiuntive e degli identificatori non canonici;
- prova integrata before/after con checksum invariato;
- tutte le richieste upstream usano una coppia route/metodo allowlisted sull'host dell'ambiente;
- bundle privo di binding D1, Queue e provider e di capability provider/HTTP/SQL generiche, recipient enumeration e deployment; unpublish resta confinato al solo tool e alla sola route approvati;
- configurazione root con
nodejs_compateglobal_fetch_strictly_public, senza override per ambiente o Service Binding; - log strutturati con request ID, tool, risorsa, esito, durata e codice bounded;
- assenza nei log di query, identificatori, contenuto, subject e credenziali;
- test di assertion valida, errata, scaduta e non autorizzata;
- metadata production canonico, DEV esplicito e assenza di fallback cross-environment;
- validazione separata con MCP Inspector e ChatGPT nell'Issue operativa.