Vai al contenuto

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: /health restituisce not_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_* o EMAIL_PROVIDER_*, mai INVALID_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_compat e global_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.

Riferimenti canonici