Skip to main content
Glama

@ingadhoc/docs-platform

The Adhoc documentation platform: one search engine, one MCP core, one access gate, and one leak guard, consumed pinned by the content repos (oba-docs, odumbo-docs, adhoc-docs).

Before this, the four pieces lived forked across the three repos: the same file with three dialects, and each fix propagated by hand — or not propagated at all. The measurement is in docs/unificacion/: lib/mcp/indice.mjs had 41 differences between the three copies, and 17 were fixes that one repo had and the other two didn't. The most expensive case: the leak guard was byte-identical in two repos and didn't exist in the third.

  • ADR 0006 from knowledge-management — one repo per content body, and the platform as a separate package: content and engine have different lifecycles and different owners.

  • ADR 0007 from knowledge-management — the gate and the leak guard belong to the platform, not to each site: a protection that each repo reimplements is a protection that some repo doesn't have.

  • Stage A of the arquitectura-plataforma-docs spec: this package, with the two versioned contracts and the drift-check that makes the pin lag visible.

How it's consumed

npm i --ignore-scripts github:ingadhoc/doc-platform#v0.1.0

Exact pin, always by tag. No ^, no main, no branches: the pin is what prevents a platform fix from breaking three sites at once, and it's what allows a one-line rollback. A range makes docs-drift-check fail on purpose — a pin that doesn't pin isn't a pin.

--ignore-scripts recommended. This package has no install script and never will; the flag is for the whole tree, because this runs in the buildCommand of public sites. Same reason the package has a single dependency (minisearch, which the search engine needs) and zero devDependencies: minimal surface in the build.

What the consumer already has and this package doesn't declare: mcp-handler and zod, which lib/mcp/mcp-handler.mjs imports. They're repo dependencies, on purpose: the repo decides which MCP framework version it deploys with, and the package doesn't impose one. All three repos have them today.

After npm i, the consuming repo is left with three lines of glue:

// api/mcp.mjs
import { crearMcp } from '@ingadhoc/docs-platform/mcp-handler';
import { crearFeedback } from '@ingadhoc/docs-platform/feedback';
import * as indice from '@ingadhoc/docs-platform/indice';
import { config } from '../docs.mcp.config.mjs';
const { handler } = crearMcp({
  config,
  indice,
  crearIssue: crearFeedback(config.feedback),
});
export default handler; // sin default export Vercel no encuentra el handler
// middleware.js — en la RAÍZ del repo (Vercel lo exige ahí)
import { next } from '@vercel/functions';
// Por RUTA RELATIVA, no por especificador de paquete: el bundler del edge
// rechaza `@ingadhoc/docs-platform/gate` cuando el repo consumidor no es
// `"type": "module"` (Docusaurus lo impide) — "unsupported modules".
// Y ojo con renombrar a middleware.mjs: el deploy queda VERDE y SIN
// middleware (la ausencia silenciosa del gate). Hallazgo del piloto
// odumbo-docs, deployment 3mXWLwPHPgcwasEji49pGEg7Lyuv.
import { decidir } from './node_modules/@ingadhoc/docs-platform/lib/mcp/gate.mjs';
const AUDIENCIAS = ['publico', 'interno']; // adhoc-docs: ['interno']
export default function middleware(request) {
  return decidir(request, process.env, { audiencias: AUDIENCIAS }) ?? next();
}
// package.json del consumidor — el guard, dentro del buildCommand
"build:publico": "node tools/build.mjs --audience=publico && npm --prefix site run build && npx docs-guard-fuga --salida=dist/publico"

The && isn't cosmetic: it's what aborts the deploy when the guard exits with 1. Don't change it to ;.

Related MCP server: Markdown RAG MCP

What it exports

Import

What it is

@ingadhoc/docs-platform/indice

search engine: buscar(), leer(), mapa() over the index the build emits. It's the only one that uses minisearch

@ingadhoc/docs-platform/mcp-handler

crearMcp({config, indice, crearIssue}): the tools, their schemas per axis, the Bearer, and the transport

@ingadhoc/docs-platform/gate

decidir(request, env, {audiencias}) / crearGate(config): the edge middleware decision

@ingadhoc/docs-platform/auth

constant-time token comparison (uses node:crypto: only on the function side)

@ingadhoc/docs-platform/tokens

the DOCS_MCP_TOKENS grammar, once, shared between the edge and the function

@ingadhoc/docs-platform/feedback

crearFeedback(config): the tool that opens the docs-feedback issue

@ingadhoc/docs-platform/config

cargarConfig() / validarConfig(): the docs.config.json validator

@ingadhoc/docs-platform/guard-fuga

correrGuard(), if you want to call it from your build instead of the bin

@ingadhoc/docs-platform/middleware

the reference middleware.js (the one that goes in the consumer's root)

bin docs-guard-fuga

the leak guard, for the buildCommand

bin docs-drift-check

the drift-check, for the consumer's CI

The two contracts

Both carry schemaVersion, and both readers throw if the emitter declares a newer version than they know how to read — or if it doesn't declare one at all. No silent degradation: a wrong index that answers badly is worse than one that doesn't answer.

  1. config ↔ platform: docs.config.json, with schema published at schema/docs.config.schema.json and validator in lib/config.mjs (own, dependency-free: ajv doesn't enter the build of a public site). The design of each field, with the measured evidence, is in docs/unificacion/diseno-eje.md; the three current configs translated, in mapeo-configs.md.

  2. index ↔ engine: emitted by each repo's tools/build.mjs and read by lib/mcp/indice.mjs. It's specified in docs/unificacion/contrato-indice.md.

The axis, in a table

The corpus declares one axis as an object: { tipo, default?, valores[] }.

eje.tipo

corpus

param in the tools

leer() without a value

wildcard (articles outside the axis)

version

oba-docs

version

picks the default and says so (elegidoPor)

yes (relacion/ applies to all)

project

adhoc-docs

project

structured ambiguity (doesn't declare default)

no

none

odumbo-docs

(not exposed)

The leer() rule is one and has no if per axis type: it only picks when the config declared who to pick. What changes behavior is the presence of eje.default, not the type — and there's a test that proves it by putting a default on a corpus with a project axis.

Running the tests

npm install && npm test        # 227 casos

bloques needs a content repo (it runs its real tools/build.mjs over the incident fixtures) and is skipped with a reason if there isn't one:

DOCS_REPO=~/repositorios/oba-docs node --test tests/bloques.test.mjs

The HTTP handler slice of mcp.test.mjs (16 cases) is also skipped with a reason if the checkout doesn't have mcp-handler/zod, which are consumer dependencies and not this package's. With both installed, mcp gives 57. A case without its capability is explicitly skipped; it doesn't run degraded.


For jjs — open decisions

What this assembly does not solve on its own. The first three are from diseno-eje.md §7 and commit the contract; the rest came out of the four analyses and are still alive after unifying.

1. A single axis per corpus: is the ceiling accepted?

schemaVersion: 1 supports one axis per config, and today that's enough for the three repos. The day a corpus needs project × version at the same time, the schema can't express it and the way out is a schemaVersion: 2 with ejes: [...] (plural). Design recommendation: accept the ceiling explicitly and let a real need reopen it with evidence (same criterion as the bump alarm in Stage B). It's your call because it commits the major.

2. metadata.types: per-corpus vocabulary or a single Adhoc one?

Today only adhoc-docs has types, and its 6 values look a lot like a knowledge-management standard (concepto, referencia, procedimiento, troubleshooting, guia, indice). If the vocabulary is Adhoc's, it doesn't go in each repo's config: it goes in the package, and the config only says whether it's required. It's a content governance decision, not a schema one; until it's settled, the schema leaves it as a per-corpus list (compatible with both outcomes).

3. The leak guard opt-out in adhoc-docs: will you sign it?

The schema requires declaring deploy.guardDeFuga, so silent omission is no longer possible. Two outcomes remain, both defensible: {"activo": false, "motivo": "…"} (that repo has no public build: its gate is unconditional, and the guard protects against leaks to the public build), or the guard goes in anyway, as a seatbelt. The motivo currently in mapeo-configs.md literally says "PENDING SIGNATURE (jjs)".

And there's a technical part that isn't fixed by copying the file (QUESTION 1 from analisis-04-seguridad.md): adhoc-docs has no :::interno blocks, doesn't emit site/generated.json with audience, and has no deploy.proyectos map. With the guard active as-is, its build fails from the start with "site/generated.json doesn't exist". The strict path is for it to emit those two things.

4. The audience list is still duplicated, and the drift-check still doesn't compare it

docs.config.json → audiences and middleware.js → AUDIENCIAS have to match, and there's no way to avoid the duplication: the edge doesn't read from the filesystem. It's exactly the kind of silent drift where the fork started. A CI case that compares them is missing (today's docs-drift-check measures the pin, not that coherence).

5. Three things to check in the repos before tagging

  • DOCS_AUDIENCE in all three environments of each Vercel project (Production, Preview, and Development) before the merge that adopts the package. With fail-closed, a project without the variable returns 503. It's the safe direction, but it isn't free.

  • --esperada in the current buildCommands: the guard now rejects it running on Vercel. If any buildCommand passes it today, that deploy starts failing. It couldn't be verified from the snapshots.

  • The MCP GET returns 503 if the deployment doesn't declare a servable audience. It's an observable change for the consumer: Claude Code's preflight receives 503 instead of the banner when the deployment is misconfigured.

6. Measured debt this package can't close

  • The preprocessor's fail-closed emits and then fails. With a misspelled directive (::: interno), build.mjs writes site/docs/** with the internal line inside and then exits with 1. Today it doesn't leak because the buildCommand chains with &&: the protection is in the operator, not in the program. It's declared as todo in tests/bloques.test.mjs, and the unification of build.mjs fixes it — which did not make it into this stage.

  • tests/bloques.test.mjs writes to <repo>/site/ because in oba and odumbo the build output is hardcoded. After running the suite you have to regenerate with npm run gen.

  • Limits of the guard's lexical approach: numbers and strings shorter than 5 characters never get a probe (a key 4821, an acronym), images are not scanned, and a leak inside applyBlocks doesn't generate a probe. It's in the guard's header; I repeat it here because it's the part that can be confused with coverage.

  • serverInfo.version is still hardcoded to '1.0.0' in the handler. It should come from the package.json of the pinned package, so an MCP client can report which version of the platform it talked to. It wasn't changed: that would be inventing behavior.

  • The wildcard is a property of the axis tipo, not of the corpus. A corpus with a project axis cannot have a cross-cutting document (eje: null stays invisible to any filter). If it's ever needed, the strict output is that the index contract prohibits it while the wildcard is off, so the contradiction fails at build time and not at runtime.

  • The spec says "vitest" as the test convention for Stage A, and none of the three repos uses vitest: the real convention —and this package's— is native node:test. It's worth fixing that line before someone installs vitest to comply with it.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides semantic search over markdown documentation using RAG, allowing natural language queries and integration with MCP clients.
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides RAG (Retrieval Augmented Generation) access to technical documentation through MCP, enabling LLMs to search and retrieve relevant documentation on-demand.
    4
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables searching, reading, and navigating MkDocs documentation sites through MCP tools for keyword, semantic, or hybrid search, document browsing, and project metadata.
    1
    BSD 2-Clause "Simplified"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ingadhoc/doc-platform'

If you have feedback or need assistance with the MCP directory API, please join our Discord server