bitacora-mcp
MCP server for creating, versioning, retrieving, and publishing HTML presentations, using git as the source of truth and Google Apps Script for deployment.
README
bitacora-mcp — Fase 3 (código local)
MCP server en NestJS para crear, versionar, recuperar y publicar presentaciones HTML. El store git es la fuente de verdad; Apps Script (Google Workspace) es el target de publicación, descartable y reconstruible desde cualquier commit. Corriendo por HTTP, la identidad es un login real de Google Workspace, no un argumento de texto libre.
Qué hace (y qué no, todavía)
- ✅
create/update/get/list/list_versions/rollback - ✅ Cada operación es un commit → historial real en git, rollback no destructivo
- ✅ Normaliza el HTML: envuelve fragments en documento completo con
<title>escapado (mismo patrónescHtmldel review de XSS) - ✅
deploy/get_deployment: publica un commit como web app de Apps Script (idempotente por versión) y consulta el estado de publicación - ✅ Transform sandbox-safe antes de publicar: fuerza
<!DOCTYPE html>,<base target="_top">y bloquea assetshttp://(mixed content) - ✅ Bootstrap HTTP alternativo (
npm run start:http) con login de Google Workspace:presentation_createusa el email autenticado comoownerreal, ignorando lo que mande el cliente. Restringido a un dominio (bidcom.com.arpor defecto). - ✅
create_from_file/update_from_file: leen el HTML directo de un archivo local, byte a byte, para decks grandes o con contenido binario embebido (imágenes en base64) que un modelo no puede reproducir de forma confiable como argumento de texto. - ⛔ Sigue corriendo solo local — el service account de dominio (en vez de cuenta personal) queda pendiente, fuera del alcance de "solo código" de este momento. Este MCP nunca se hostea en un cluster propio: lo único que se publica en infraestructura externa son las presentaciones, como web apps de Google Workspace vía Apps Script.
Arquitectura interna
Los módulos mapean 1:1 a los futuros packages/ del monorepo:
core/→ normalización y validación de HTML (DeckService,escHtml)store/→GitSpecStore, el store versionado en git (decks + índice de deployments)presentations/→ orquestación +@McpControllercon las 6 tools de versionadodeployer/→ toda la fricción de Google (Apps Script) en un lugar: OAuth, cliente de la Apps Script API, transform sandbox-safe, y las 2 tools de publicaciónauth/→ guard de dominio + config del servidor de autorización OAuth para el bootstrap HTTPshared-tools.module.ts→ controllers/providers de las 8 tools, importado tanto por el bootstrap stdio (app.module.ts) como por el HTTP (http-app.module.ts) para no duplicar la lista
Correr
npm install
npm run build
npm start # levanta el server por stdio
El store se crea en ~/.bitacora-store (configurable con DECK_STORE_DIR).
Publicar en Apps Script (setup de Google, una sola vez)
El deploy corre bajo cuenta personal en esta fase (Fase 3 migra a service account de dominio). Pasos previos, una sola vez por máquina/cuenta:
- En Google Cloud Console, un proyecto (o uno nuevo).
- Habilitar la Google Apps Script API en ese proyecto (APIs & Services → Library).
- Credentials → Create credentials → OAuth client ID, tipo Desktop app.
- Descargar el JSON y guardarlo en
~/.bitacora-google/oauth-client.json(override conGOOGLE_OAUTH_CLIENT_PATH). - Correr el consent flow local:
Abre una URL de consentimiento, levanta un server local (loopback) para recibir el redirect, y cachea el refresh token ennpm run build npm run google:authorize~/.bitacora-google/token.json(override conGOOGLE_TOKEN_PATH). Se refresca solo de ahí en más.
Las credenciales de Google se guardan fuera del store git a propósito
(~/.bitacora-google/, no DECK_STORE_DIR): el store se versiona y podría
compartirse; nada con secretos debe vivir ahí.
Variables opcionales para el manifest del web app:
| Env var | Default | Qué controla |
|---|---|---|
APPS_SCRIPT_WEBAPP_ACCESS |
DOMAIN |
Quién puede abrir la URL publicada (DOMAIN, ANYONE, ANYONE_ANONYMOUS, MYSELF) |
APPS_SCRIPT_WEBAPP_EXECUTE_AS |
USER_DEPLOYING |
Con qué identidad corre el doGet (USER_DEPLOYING o USER_ACCESSING) |
Prueba end-to-end
npm run smoke # cliente MCP que ejercita todo el ciclo sobre stdio
El smoke test corre con DECK_DEPLOYER_MOCK=1: el ciclo deploy /
get_deployment se ejercita contra un cliente de Apps Script en memoria, sin
tocar Google ni requerir credenciales. Para probar contra la API real, corré
el server con DECK_DEPLOYER_MOCK sin setear (o en 0) y las credenciales de
la sección anterior ya cacheadas.
Correr con login de Google Workspace (HTTP, local)
Bootstrap alternativo al stdio de siempre: el server escucha por HTTP y exige un login real de Workspace antes de dejar usar cualquier tool. Pensado para probar el flujo de identidad localmente antes de decidir dónde corre este proceso para uso remoto (Fase 3 completa, fuera de alcance por ahora) — nunca en un cluster propio como EKS; lo único que este proyecto publica en infraestructura externa son las presentaciones, vía Apps Script.
Es un segundo OAuth client, distinto del "Desktop app" que ya usa el deployer de Apps Script — este es para loguear usuarios, no para llamar a una API:
- En el mismo proyecto de Google Cloud Console → Create Credentials → OAuth client ID → tipo Web application (¡no Desktop app! ese tipo no tiene redirect URI configurable).
- Authorized JavaScript origins:
http://localhost:3030(sin path, sin/final). - Authorized redirect URIs:
http://localhost:3030/auth/callback(con path, exacto). - Anotá el Client ID y el Client secret (o descargá el JSON).
Variables de entorno para levantar el bootstrap HTTP:
| Env var | Requerida | Qué es |
|---|---|---|
GOOGLE_WORKSPACE_CLIENT_ID |
sí | Client ID del OAuth client "Web application" de arriba |
GOOGLE_WORKSPACE_CLIENT_SECRET |
sí | Su client secret |
JWT_SECRET |
sí | Firma los tokens que emite el servidor de autorización propio. Generá uno con openssl rand -hex 32 (mínimo 32 caracteres) |
WORKSPACE_DOMAIN |
no (default bidcom.com.ar) |
Dominio al que se restringen los logins — cualquier otra cuenta de Google recibe 403 |
MCP_SERVER_URL |
no (default http://localhost:3030) |
Base URL del server |
PORT |
no (default 3030) |
Puerto HTTP |
npm run build
export GOOGLE_WORKSPACE_CLIENT_ID=...
export GOOGLE_WORKSPACE_CLIENT_SECRET=...
export JWT_SECRET=$(openssl rand -hex 32)
npm run start:http
El server queda escuchando en http://localhost:3030/mcp, con los endpoints
OAuth estándar del servidor de autorización embebido bajo /auth/* y
/.well-known/* (discovery). Un cliente MCP real (Claude, MCP Inspector) hace
todo el baile de login solo; para un chequeo manual sin un cliente a mano, un
cliente OAuth de prueba (registro DCR + PKCE + tools/call) sirve para
verificar que owner termina siendo el email autenticado, no lo que mande
quien llama.
Por qué dos capas de OAuth: Google no soporta Dynamic Client
Registration, que es justo lo que un cliente MCP remoto (Claude) necesita
para autoregistrarse contra el servidor de autorización sin configuración
previa. Apuntar un cliente MCP directo a Google como authorization server
rompe con mcp_registration_failed — ya nos pasó antes. La solución (la que
usa este server) es que nuestro propio servidor de autorización (el
McpAuthModule de @rekog/mcp-nest-auth) hable el protocolo OAuth 2.1/MCP
completo con el cliente MCP (DCR, PKCE, discovery), y delegue solo el login
a Google por dentro. El cliente MCP nunca sabe que Google existe.
Conectar a Claude Desktop
En claude_desktop_config.json:
{
"mcpServers": {
"bitacora": {
"command": "node",
"args": ["/RUTA/ABSOLUTA/bitacora-mcp/dist/main.js"],
"env": { "DECK_STORE_DIR": "/RUTA/ABSOLUTA/deck-store" }
}
}
}
Decks grandes
Hay dos problemas distintos detrás de "el HTML es grande", con soluciones distintas:
1. El HTML ya existe como archivo en disco. Usá create_from_file /
update_from_file — el server lo lee directo del filesystem, byte a byte. El
modelo nunca ve ni reproduce el contenido, así que no importa cuán grande sea
ni si tiene imágenes en base64 embebidas: no hay riesgo de truncamiento ni de
corrupción por transcripción.
presentation_create_from_file({ path: "/ruta/absoluta/deck.html", title, owner })
presentation_deploy({ id })
2. El HTML lo está generando el modelo mismo (no existe como archivo) y es demasiado grande para una sola tool call — el límite real lo pone el cliente MCP emitiendo el argumento (en la práctica, unos ~40 KB por llamada), no este server. Para ese caso existe la carga por chunks:
presentation_create({ title, html: chunk0, owner, partial: true }) -> { id }
presentation_append({ id, html: chunk1, done: false }) // repetir N veces
presentation_append({ id, html: chunkN, done: true }) // cierra y comitea
presentation_deploy({ id }) // igual que siempre
Con partial: true no se toca git todavía — solo se reserva el id y se
guarda el HTML recibido en memoria. Recién en el done: true corre la
normalización de siempre (Fase 1) y se comitea, exactamente como si hubiera
llegado en una sola llamada. Una carga sin cerrar (sin done: true) no deja
rastro en el store; se pierde si el proceso reinicia, que es aceptable porque
solo afecta a esa carga en curso, no a decks ya guardados.
Chunking no resuelve el problema (1): un modelo tiene que regenerar cada
byte del argumento de cada chunk, y para contenido grande con base64 embebido
eso es un problema de fidelidad de transcripción, no de tamaño — por eso
existe create_from_file como camino aparte.
Tools
| Tool | Qué hace |
|---|---|
presentation_create |
Crea un deck y lo guarda versionado. Devuelve id + version (SHA). Con partial: true, reserva el id y guarda el HTML recibido como primer chunk sin comitear nada — hay que cerrar con presentation_append. |
presentation_create_from_file |
Igual que presentation_create, pero lee el HTML de un archivo local (path absoluto) en vez de tomarlo como argumento. Para decks grandes o con base64 embebido. |
presentation_append |
Agrega un chunk de HTML a una carga iniciada con presentation_create({partial: true}). done: true en el último chunk cierra, valida y comitea el deck completo. Para decks grandes que no entran en una sola tool call. |
presentation_update |
Nueva versión con HTML y/o metadata nuevos. |
presentation_update_from_file |
Igual que presentation_update, pero lee el HTML nuevo de un archivo local. |
presentation_get |
HTML + metadata en HEAD o en un version (SHA) histórico. La descripción de la tool le pide al asistente que muestre el html como Artifact en vez de texto/código. |
presentation_list |
Lista los decks, filtrable por owner. |
presentation_list_versions |
Historial de commits de un deck. |
presentation_rollback |
Vuelve a un version anterior creando un commit nuevo. |
presentation_deploy |
Publica un version (default HEAD) como web app de Apps Script. access opcional controla quién puede verla (MYSELF/DOMAIN/ANYONE/ANYONE_ANONYMOUS, default DOMAIN). Idempotente por commit + access. |
presentation_get_deployment |
Devuelve el estado de publicación actual (commit, scriptId, deploymentId, url). |
Notas de stack
@rekog/mcp-nestv2 — APIMcpStrategy+@McpController(noMcpModule.forRoot).- Transporte stdio:
logger: falseporque stdout está reservado para el protocolo. @rekog/mcp-nest-auth— servidor de autorización OAuth 2.1/MCP embebido (McpAuthModule), conGoogleOAuthProviderdelegando el login. Solo se usa en el bootstrap HTTP (http-app.module.ts/main-http.ts); el stdio (app.module.ts/main.ts) no lo toca.- Bajo stdio no hay request HTTP, así que ninguna tool ve un usuario
autenticado (
@McpUser()daundefined) — es el comportamiento esperado para uso local en Claude Desktop, documentado por la propia librería.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。