Skip to main content
Glama

@ingadhoc/docs-platform

Adhoc's 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. The measurement is in docs/unificacion/: lib/mcp/indice.mjs had 41 differences among 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 won't have one; 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 version of the MCP framework it deploys with, and the package doesn't impose one. All three repos have them today.

After npm i, the consumer 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';
export const { handler, default: fetchHandler } = crearMcp({
  config,
  indice,
  crearIssue: crearFeedback(config.feedback),
});
// middleware.js — en la RAÍZ del repo (Vercel lo exige ahí)
import { next } from '@vercel/functions';
import { decidir } from '@ingadhoc/docs-platform/gate';
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 ;.

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 per-axis schemas, the Bearer, and the transport

@ingadhoc/docs-platform/gate

decidir(request, env, {audiencias}) / crearGate(config): the edge middleware's 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. 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, no dependencies: ajv doesn't enter a public site's build). The design of each field, with 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 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 chooses when the config declared who to choose. What changes behavior is the presence of eje.default, not the type — and there's a test that proves it by giving a default to a corpus with a project axis.

Running the tests

npm install && npm test        # 227 casos

bloques needs a content repo (it actually runs its tools/build.mjs on 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, not this package's. With both installed, mcp gives 57. A case without its capability is explicitly skipped; it isn't run degraded.


For jjs — open decisions

What this assembly doesn't 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. One 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 once, the schema doesn't express it and the way out is a schemaVersion: 2 with ejes: [...] (plural). Design recommendation: accept the ceiling explicitly and let real need reopen it with evidence (same criterion as the Stage B bump alarm). It's your call because it commits the major.

2. metadata.types: per-corpus vocabulary or Adhoc-wide?

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 requires it. 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: do you sign it?

The schema requires declaring deploy.guardDeFuga, so silent omission is no longer possible. Two ways out remain, both defensible: {"activo": false, "motivo": "…"} (that repo has no public build: its gate is unconditional, and the guard protects against leakage to the public build), or the guard goes in anyway, as a belt. The motivo currently in mapeo-configs.md literally says "PENDIENTE DE FIRMA (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 because "site/generated.json doesn't exist". The strict one 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 the 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's not free.

  • --esperada in the current buildCommands: now the guard 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 listed as a todo in tests/bloques.test.mjs, and the unification of build.mjs fixes it — which was not included in 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 4821 key, an acronym), images are not scanned, and a leak inside applyBlocks doesn't generate a probe. It's in the guard's header; I'm repeating 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 pinned package's package.json, so an MCP client can report which platform version it talked to. It wasn't changed: that would be inventing behavior.

  • The wildcard belongs to the axis tipo, not 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 forbids it while the wildcard is off, so the contradiction fails at build time and not at runtime.

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

-
license - not tested
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • Query any docs site via MCP. Submit a URL, ask questions, get cited answers.

  • A paid remote MCP for Context7 MCP docs, built to return verdicts, receipts, usage logs, and audit-r

  • Knowledge coverage map and health score. Ingest docs into a governed knowledge graph via MCP.

View all MCP Connectors

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