@bwilliamson/mdcp-core
v0.4.1
Published
MDCP core library — MarkDown Context Protocol compile, refs, validation
Maintainers
Readme
@bwilliamson/mdcp-core
Why MDCP
LLM pair-coding on a real repo breaks down when documentation is a single monolith, unvalidated, and tangled up with implementation. Merge conflicts stack up on one giant README. Agents guess #anchor slugs that rot after the next edit. Every turn dumps the whole guide into context. Shards and published output drift apart silently. A one-off bash script holds it together until nobody owns it.
The usual fixes do not solve that
| Approach | What it misses |
| ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| Monolithic README / full llms.txt | No sharding, no validation gate, no stable refs registry |
| Context7 / large crawled corpora | Fuzzy retrieval — not author-controlled, deterministic, or PR-reviewable |
| Cursor rules / AGENTS.md | Host-native friction hints, not validated product context in git |
| Docusaurus / MkDocs / VitePress | Strong public doc sites — weak agent-first refs lookup, scoped export, and CI structural gates |
| MCP filesystem reads | Delivers whatever exists; does not enforce shard discipline at authoring time |
MDCP is complementary to MCP and doc-site generators: it owns authoring, compile invariants, and the validation gate upstream of delivery. See Scope and positioning.
What MDCP does not replace
MDCP is a middle layer in your stack — not a substitute for what sits above or below it:
- Ephemeral work docs — sprint plans, task briefs, spike notes, and scratch docs that help turn meta ideas into code. Those stay temporary and task-scoped; mdcp shards hold durable product context that outlives a single PR or agent session.
- Orchestrators and agent systems — Cursor rules, MCP servers, CI pipelines, and multi-agent coordinators still run your workflow. MDCP feeds them validated, scoped documentation context; it does not replace how they schedule, route, or hand off work.
- Checked-in prompts and playbooks — many teams already version agent prompts, rules files, and workflow templates in git. MDCP complements that habit with a formal, open framework: validated product-context shards, compile/check gates,
refs lookup, and versioned task prompts — so prompt libraries and durable documentation share the same discipline. - Implementation — code, tests, and config remain the source of truth for behavior. Shards carry intent, constraints, and acceptance criteria — not line-by-line instructions that duplicate the repo.
The goal is to reduce friction between durable context and active work: smaller documentation batches, fewer context-switching interruptions, and less time re-explaining the system each turn — so humans and agents stay closer to flow state.
Adopt it today
The open-alpha CLI and core library are a working foundation, not a slide deck:
- Ship faster with agents —
mdcp refs lookupresolves link targets from compiled output;mdcp export --llmscopes context to what the next turn needs instead of re-sending the entire README. - Stop doc drift before merge —
mdcp checkruns the same compile → refs → xrefs pipeline for agents, CI, and human reviewers. - Edit docs like code — small shards, manifest order, one compile step; publish to monolith,
DEVELOPERS.md, or npm READMEs from the same source. - Keep plan separate from implementation — shards hold context and the high-level plan; code holds how. Structure enforces that split.
Integrate programmatically with @bwilliamson/mdcp-core for compile, refs, validation, and LLM export in CI, editors, and custom tooling. Consumer workflow: Why mdcp for coding agents, Alternatives and adoption.
So what — how do I use this in my project?
Start with @bwilliamson/mdcp-cli in any repository — monorepo or single app, any language or framework. mdcp cares about your documentation shards and compile pipeline, not your application architecture. Add @bwilliamson/mdcp-core later when you need programmatic compile, refs, or export in CI or custom tooling.
npm install -D @bwilliamson/mdcp-cli- Copy getting-started-with-mdcp.prompt.md (or load it from
.caches/mdcp/prompts/after fetch), fill inFEATURE=andPERSONA=, and send it to your coding agent — it inspects the repo and walks through config, shard layout, and firstmdcp check.
Fetch the bootstrap index and prompts into your docs root:
npx @bwilliamson/mdcp-cli export --llms-index --fetch --fetch-profile alpha --fetch-ref v0.4.1 --docs-root docsCLI walkthrough: Install and quick start.
Where it is going
Like OpenAPI standardized HTTP API contracts, MDCP is evolving into an open contract for documentation context — intent, design, and terminology you can share with other systems. That benefits inter-agent development (validated shards and glossaries instead of re-crawling ad hoc prose) and human-in-the-loop verification: reviewers read the same compiled context agents use and confirm the system behaves as documented. Roadmap: Vision and roadmap.
Overview
Core library for mdcp — compile sharded Markdown guides, build section link registries, validate structure, and export LLM-friendly output.
Use this package when you need mdcp behavior in scripts, CI pipelines, editors, or other tools without shelling out to the CLI.
Requirements
- Node.js >= 24.0.0
Install
npm install @bwilliamson/mdcp-coreThe CLI (@bwilliamson/mdcp-cli) depends on this package. Install @bwilliamson/mdcp-core directly only when you need the programmatic API.
Stability
Pre-1.0 / open alpha (0.4.0): There is no API stability guarantee until 1.0.0. Exported functions, types, mdcp.config.json schema, and compile output may change in any 0.x.y release. Pin @bwilliamson/[email protected] and read package changelogs before upgrading.
Glossary
Shared acronyms and terms for all mdcp docs. Spell out on first use in a shard and link the short form here.
Each term is its own shard under docs/glossary/. For large glossaries, split manifests across sub-index files (for example index-protocol.md, index-format.md) and set compile.scopeRoot to glossary so transitive links pull term shards into other guides. Read domain glossary.
Protocol terms
Format and compile terms
Quick example
import {
loadConfig,
compileGuides,
resolveDocsRoot,
genRefsFromCompiled,
resolveRefsPath,
lookupHeadings,
buildSlugRegistry,
stripForLlm,
getLlmExportOptions,
} from '@bwilliamson/mdcp-core';
const docsRoot = '/path/to/docs';
const config = loadConfig('mdcp.config.json', docsRoot);
const compiled = compileGuides({
guidesRoot: resolveDocsRoot(config, docsRoot),
compileOrder: config.compileOrder,
banner: config.banner,
guides: config.guides,
docsRoot,
config,
});
const refsPath = resolveRefsPath(docsRoot, config.outputDir, config.refs.registryFile);
genRefsFromCompiled(compiled, refsPath);
const registry = buildSlugRegistry(compiled);
const matches = lookupHeadings(registry, 'authentication');
const llmText = stripForLlm(compiled, getLlmExportOptions(config));Use writeCompiledGuides when you need to write the monolith and per-guide publish outputs to disk.
API — Config
| Export | Purpose |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| loadConfig(path, configBase) | Load and validate mdcp.config.json (path is resolved from configBase) |
| resolveOutputPath, resolveRefsPath, resolveGuideDir, defaultGuideOutputFile | Path resolvers for docs root and outputDir |
| getGuideConfig, guideScanDirs, shardLintPaths, xrefScanDirs | In-scope guide fileset and xref scan helpers |
| MdcpConfigSchema, MdcpConfig, MdcpConfigInput, GuideConfig, GuideConfigInput | Zod schema and types |
| DEFAULT_COMPILE_HOOKS, resolveCompileHooks | Default built-in hook pipeline and guide-level resolution |
Path resolution: configBase vs docs root
| Concern | Base | Example |
| -------------------------- | ----------------------------- | ----------------------------------------------------------------------------- |
| Finding mdcp.config.json | configBase (invocation dir) | --config docs/mdcp.config.json → <repo>/docs/mdcp.config.json |
| Guide shards (default) | Docs root (--docs-root) | resolveGuideDir('features', config, docsRoot) → <docsRoot>/features |
| outputDir | Docs root | _build → <docsRoot>/_build |
| All generated paths | outputDir | features.md → <docsRoot>/_build/features.md; .caches/refs.json for refs |
All generated paths use resolveUnderOutputDir(docsRoot, outputDir, file) — relative to outputDir unless file is absolute. Details: API — Refs.
import { loadConfig, resolveGuideDir } from '@bwilliamson/mdcp-core';
const config = loadConfig('docs/mdcp.config.json', process.cwd());
const featuresDir = resolveGuideDir('features', config, join(process.cwd(), 'docs'));Pass process.cwd() as configBase for loadConfig. Pass the docs root as docsRoot to resolveGuideDir, resolveOutputPath, and resolveRefsPath.
Consumer path table: Config essentials — path layout.
Defaults: outputDir _build; per-guide outputs {name}.md (or guide.md when one guide); optional monolith when outputFile is set; refs at .caches/refs.json.
compile.outputFile overrides a guide's output path (relative to outputDir or absolute). Guides with an explicit compile.outputFile are excluded from an optional monolith.
compile.includeBanner controls whether the global banner is prepended (defaults to false for per-guide outputs).
Publish outputs and link paths
Guides with compile.outputFile publish outside the shard tree (npm READMEs, DEVELOPERS.md, and similar). Shard-authored ../ links are rebased automatically:
- Resolve each link from the shard file to an absolute path
- Emit a path relative to the publish output file
No per-guide path-prefix config — output location and shard path supply the geometry. See Publish-relative link rewriting for intent, pass ordering, and dogfood examples.
Intra-guide ./section.md links still rewrite to #anchor on every compile (post-stitch pass).
compile.hooks
Built-in hooks run by default when compile.hooks is omitted. See Default compile hooks.
- Omitted — run
DEFAULT_COMPILE_HOOKSin order:stripAnchors,codeEvidence,inlineInserts string[]— explicit override; replaces defaults entirely (backward compatible)Record<string, boolean>— opt out; keys withfalseremove that hook from defaults
Optional per-hook settings: compile.hooksConfig (inlineInserts.searchRoots). Post-stitch anchor stripping: compile.stripAnchors (default true), independent of the per-shard stripAnchors hook unless opted out.
compile.crossGuideLinks
Assembly-time cross-guide link options on the compiling guide (not a compile hook):
ignoreGuides—string[]of guide names whose cross-guide shard links keep source.mdpaths instead of rewriting to monolith#slugtargets
See Cross-guide link rewriting and ignoreGuides.
API — Compile
| Export | Purpose |
| ------------------------------------------------- | ---------------------------------------------------- |
| compileGuides, compileGuideResults | Stitch shards into monolith text |
| writeCompiledGuides | Write monolith and publish outputs to disk |
| writeOutputFile, resolveBackupPath | Opt-in backup before overwrite; backup path resolver |
| resolveBackupOptions | Merge config and CLI backup settings |
| WriteOutputBackupOptions | Backup options type |
| sectionFiles, processSection, assembleGuide | Lower-level assemble pipeline |
| formatCompileTitle, extractFirstHeading, … | Optional compile.title injection and deduplication |
| demoteHeadings, stripAboutThisGuideHeading, … | Heading transforms |
| registerCompileHook, applyCompileHooks | Extension hooks (stripAnchors, inlineInserts, …) |
compileGuides returns monolith text only — guides with compile.outputFile are excluded. writeCompiledGuides writes both the monolith and any publish targets.
writeOutputFile writes compile or export targets. Default: overwrite. When backup.enabled is true, moves an existing file to {outputDir}/{backupDir}/{docsRoot-relative-key}{ext} before writing. Pass backup on CompileOptions or resolve via resolveBackupOptions(config, cliOverrides).
When compile.title is set, assembleGuide injects a ## heading followed by a blank line before the first section. See API — Config for per-guide compile fields and top-level backup config.
Full spec: Compile output backup.
API — Refs and validation
Refs (cross-links)
| Export | Purpose |
| -------------------------------------------------------------- | --------------------------------------- |
| headingTextToPlain, githubSlugify, buildSlugRegistry | GitHub heading slugs via github-slugger |
| lookupHeadings | Fuzzy search over refs.json headings |
| genRefsFromCompiled, readRefsRegistry, checkRefsRegistry | refs.json lifecycle |
| resolveRefsPath, writeRefsRegistry | Path and I/O helpers |
resolveRefsPath(docsRoot, outputDir, registryFile)
Resolves the on-disk path for the refs registry. Implemented via the same outputDir-relative helper as resolveOutputPath. Pass the docs root (the CLI --docs-root value).
outputDiris relative todocsRoot.registryFileis relative tooutputDir(default.caches/refs.json).
resolveRefsPath('/docs', '_build', '.caches/refs.json');
// → /docs/_build/.caches/refs.json
resolveRefsPath('/docs', '.', 'refs.json');
// → /docs/refs.jsonPrefer outputDir-relative values in config (for example ".caches/refs.json" when outputDir is "_build"). See Config essentials — path layout.
Heading slugs (github-slugger)
githubSlugify, headingTextToPlain, and buildSlugRegistry derive #fragment targets from compiled headings using github-slugger, which matches GitHub's html-pipeline TableOfContentsFilter. GFM does not define auto-generated heading IDs; treat github-slugger parity as the contract.
| Export | Purpose |
| -------------------- | ---------------------------------------------------- |
| headingTextToPlain | Strip ids and inline markup before slugging |
| githubSlugify | Single-heading slug via github-slugger |
| buildSlugRegistry | Document-wide slugs; duplicates get numeric suffixes |
import { githubSlugify, headingTextToPlain } from '@bwilliamson/mdcp-core';
headingTextToPlain('**Authored GFM** ``');
// → 'Authored GFM'
githubSlugify('Preprocessor / templating (out of scope)');
// → 'preprocessor--templating-out-of-scope'
githubSlugify('`--config` vs `--docs-root`');
// → '--config-vs---docs-root'Consumer docs: Cross-links and refs — heading slugs.
Manifest
| Export | Purpose |
| ------------------------------- | ---------------------------------------------- |
| sectionFiles, assembleGuide | Resolve compile order from manifest link order |
Validation
| Export | Purpose |
| ----------------------- | --------------------------------------- |
| checkOrphansForGuides | Detect unlinked or missing shard files |
| lintXrefs | Chapter-style cross-reference detection |
API — Export, shard, and peers
Export
| Export | Purpose |
| ------------------------------------ | --------------------------------- |
| stripForLlm, getLlmExportOptions | Token-optimized output for agents |
Shard (split)
| Export | Purpose |
| -------------------------------- | --------------------------------------- |
| shardFromMonolith, runMdTree | Split a monolith into guide directories |
Peer tools
| Export | Purpose |
| --------------------------- | ----------------------------------------------------------------- |
| findPeerBinary, runPeer | Locate and run host-repo linters (markdownlint-cli2, vale, …) |
Peer linters are not bundled. Detection order: node_modules/.bin → PATH → skip with info.
Compile hooks
Per-shard transforms run during assembleGuide before sections are stitched. Hooks receive each shard body after heading demotion and preamble stripping; assembly-time passes (cross-guide rewrite, publish-relative rewrite on compile.outputFile outputs, anchor stripping, intra-guide rewrite) run around the hook pipeline.
Hooks assemble authored GFM — not variable substitution or template logic. See Preprocessor / templating (out of scope).
Architecture
assembleGuide (per guide)
│
├─ for each manifest shard (in order)
│ ├─ processSection (demote headings, strip about-this-guide)
│ ├─ applyCompileHooks (named hooks from config, in order)
│ ├─ rewriteCrossGuideFileLinks (automatic when link index present)
│ └─ rewritePublishRelativeLinks (when compile.outputFile is set)
│
└─ stitch → stripAnchors (default) → intra-guide .mdGuide link index — built once per compileGuideResults from every guide in compileOrder (manifest sections plus transitively linked shards). Used by the automatic cross-guide pass. Optional compile.crossGuideLinks.ignoreGuides on the compiling guide skips monolith rewrite for listed targets. See Cross-guide link rewriting.
Per-guide hook state — mutable hookState on CompileHookContext (for example inlineInserts counters and first-anchor map) shared across shard invocations within one guide compile.
Path resolution — hooks and assembly passes resolve relative paths from dirname(sourceFile) first, then guideDir, then compile.scopeRoot. Publish outputs rebase remaining ../ file links per shard via absolute-path resolution — see Publish-relative link rewriting. Cross-guide and codeEvidence use the same resolve-then-rebase model for their link classes.
Extension pattern
Register custom hooks in consumer or library code:
import { registerCompileHook } from '@bwilliamson/mdcp-core';
registerCompileHook('myHook', (ctx) => {
return ctx.body.replace(/TODO/g, 'DONE');
});Custom hooks are not in the default pipeline — list them explicitly in compile.hooks when needed.
Hook implementations should be pure on ctx.body except when intentionally using shared hookState. Leave unmatched links unchanged. Prefer docs-first specs with tests mapped to spec sections (see each hook shard below).
Configuration
Built-in hooks run by default. Omit compile.hooks for the common case. Optional per-hook config lives under guides[].compile.hooksConfig.
{
"name": "glossary",
"compile": {
"scopeRoot": ".",
"outputFile": "glossary.md",
"hooksConfig": {
"inlineInserts": { "searchRoots": ["diagrams"] }
}
}
}Optional assembly-time cross-guide exceptions: compile.crossGuideLinks.ignoreGuides on the compiling guide — see Cross-guide link rewriting.
Opt out per hook
Disable specific defaults with an object on compile.hooks (false removes a hook):
{
"compile": {
"hooks": { "inlineInserts": false }
}
}Explicit override
Replace the entire default pipeline with a string array (backward compatible):
{
"compile": {
"hooks": ["stripAnchors", "codeEvidence"]
}
}Default hook order and behavior: Default compile hooks. Config API: API — Config.
For manifest compile order and compile.sectionsHeading, see Manifest compile order.
Built-in hooks
stripAnchors— per shard (also default post-stitch). Removes explicit anchor markers.codeEvidence— per shard. codeEvidence: repo source links →#Lfragments.inlineInserts— per shard. inlineInserts: inline captioned insert libraries.- Cross-guide rewrite (assembly) — per shard before stitch. Cross-guide links: automatic from
compileOrder; optionalcrossGuideLinks.ignoreGuides. - Publish-relative rewrite (assembly) — per shard before stitch when
compile.outputFileis set. Publish-relative links: resolve shard links to absolute paths, emit paths relative to the publish file.
stripAnchors is also controlled by compile.stripAnchors (default true) after assembly.
codeEvidence
Specification for the codeEvidence compile hook. Tests in packages/mdcp-core/test/code-evidence.test.ts map to the sections below (docs first, then TDD).
codeEvidence purpose
Architecture and technical review shards cite repo source files as evidence. At compile time, the hook:
- Resolves line ranges from link text (for example
L6-L8,lines 12–15,:42) - Resolves symbols from the URL fragment (
file.ts#symbol) or from the link label when no fragment is present (for example[`orgCount`](../../functions/src/foo.ts)) - Appends GitHub-style
#Lfragments (#L6,#L6-L8) to the link target - Rewrites the target path to be relative to the rendered output — the per-guide
compile.outputFilewhen set, otherwise the monolith path (outputDir+outputFilefrom config)
Publish outputs (compile.outputFile) rewrite remaining relative file links automatically (for example ../../package.json → package.json in DEVELOPERS.md). Same resolve-then-rebase model as publish-relative assembly; see Publish-relative link rewriting.
codeEvidence link matching
A link is rewritten when all of the following hold:
- Standard markdown link syntax:
[label](path) - Target path is a source file (common extensions such as
.ts,.py,.go, or extensionless paths likeMakefile) - Target is not
http://,https://, or#…
Markdown (.md) links, external URLs, and same-guide shard links are left unchanged.
codeEvidence line ranges
Line ranges are parsed from the link label first, then from the path (before any # fragment). Supported forms:
| Form in label or path | Fragment |
| --------------------- | ------------------ |
| L6-L8, L6–L8 | #L6-L8 |
| L42, line 42 | #L42 |
| :10-20, :10 | #L10-L20, #L10 |
If the URL already has a normalized #L… fragment, the hook preserves it (normalizing case to #L).
codeEvidence symbols
When no line range is found:
- If the URL has a
#fragmentthat is not already#L…, treat the fragment as a symbol name and scan the resolved source file for a matching declaration or reference. - Otherwise, treat the link label as the symbol (backticks and surrounding whitespace stripped).
Symbol lookup scans for identifier matches and common declaration forms (function, class, const, export, call sites).
codeEvidence path resolution
Source file lookup order:
- Relative to the current shard directory
- Relative to the shard parent directory
process.cwd()and its parentcompile.scopeRoot(when set on the guide — same field used for manifest scoping)
When a source file is resolved, the hook rewrites the link target to a POSIX path relative to the rendered output document, preserving any #L… fragment added by the hook. No hook-specific config is required: shard-relative paths in source are resolved as written, then rebased for where the compiled file lands.
codeEvidence exclusions
The hook does not transform:
- Markdown shard links (
.md) - External URLs
- Source links when the file cannot be resolved and no line range appears in label or path
- Body text when
codeEvidenceis disabled viacompile.hooks: { "codeEvidence": false }or an explicit hook override that omits it
codeEvidence config
Runs by default — no hook list required. Path rewriting uses the monolith or per-guide output path automatically:
{
"name": "architecture-review",
"compile": {}
}When the guide publishes to its own file instead of the monolith, set compile.outputFile (paths are rebased to that file). When shards link across directories outside the guide tree, set compile.scopeRoot (typically "." for repo root) so manifest scoping and evidence lookup share one root:
{
"name": "architecture-review",
"compile": {
"scopeRoot": ".",
"outputFile": "architecture-review.md"
}
}Opt out: "hooks": { "codeEvidence": false }. See Default compile hooks.
codeEvidence compile example
Shard input (under review/claim.md):
Evidence: [`orgCount`](../../functions/src/foo.ts)
See [firestore.rules L6-L8](../../firestore.rules#L6-L8).Compiled output (when functions/src/foo.ts defines orgCount on line 6 and output is architecture-review.md at repo root):
Evidence: [`orgCount`](functions/src/foo.ts#L6)
See [firestore.rules L6-L8](firestore.rules#L6-L8).inlineInserts
Specification for the inlineInserts compile hook. Tests in packages/mdcp-core/test/inline-inserts.test.ts map to the sections below (docs first, then TDD).
inlineInserts purpose
Guides link to captioned insert shards (.md files) in typed libraries under the docs root. Shard bodies may be markdown tables, prose, or media embeds (images, video, audio). At compile time, the hook:
- Inlines insert markdown at the first link (per guide, per file)
- Adds a numbered
####heading (GFM only — no HTML, no directives) - Rewrites later links to the same file as markdown back-links (
[label](#slug))
inlineInserts layout
One library directory per insert type (library-science convention):
docs/
diagrams/ # flow charts, sequence diagrams (markdown tables or images)
tables/ # reference tables, comparison matrices
figures/ # screenshots, static diagrams
media/ # video, audio, and other captioned media embeds
inserts/ # optional generic captioned blocks
review/
insert-catalog.md # links to ../diagrams/…, ../tables/…, ../figures/…, ../media/…Link targets are always .md insert shards. Put binary assets alongside the shard (or under the same library) and reference them from the shard body — for example  or an HTML <video> / <audio> block when your renderer supports it.
Shards link with normal markdown — no <!-- directives -->.
inlineInserts link matching
A link is an insert reference when all of the following hold:
- Standard markdown link syntax:
[label](path) - Target path contains
diagram,diagrams,table,tables,figure,figures,media,insert, orinserts - Target ends in
.md(optional#fragmentsuffix is ignored for file lookup) - Target is not
http://orhttps://
inlineInserts exclusions
The hook does not transform:
- Regular shard links (for example
./intro.md,../glossary/term.md) - Direct links to binary assets (for example
../figures/architecture.png,../figures/demo.mp4) — use a captioned.mdinsert shard that embeds the media instead - External URLs, even when the path contains
diagrams/ - Links to missing insert files (left unchanged)
- Body text when
inlineInsertsis disabled viacompile.hooks: { "inlineInserts": false }or an explicit hook override that omits it
inlineInserts first inline
The first reference to an insert file (document order across all shards in the guide) is replaced with:
#### {Kind} {n}. {caption}
{insert shard body — tables, prose, images, video, audio, …}- Kind —
Diagram,Table,Figure,Media, orInsert(from parent library directory) - n — serial number for that kind in this guide (see Numbered captions below)
- caption — link label, or a humanized basename when the label is empty
- Anchor slug — GitHub-style slug of the full heading (for example
Table 1. Status codes→#table-1-status-codes)
Output uses GFM headings and back-links for captions. Inlined shard bodies pass through as written (markdown tables, , or HTML <video> / <audio> when your renderer supports them).
inlineInserts numbered captions
Serial counters are per insert kind and per guide compile:
| Kind | First inline heading example | Second inline (same kind) |
| ------- | ------------------------------ | ------------------------- |
| diagram | #### Diagram 1. Request flow | #### Diagram 2. … |
| table | #### Table 1. Status codes | #### Table 2. … |
| figure | #### Figure 1. Component map | #### Figure 2. … |
| media | #### Media 1. Walkthrough | #### Media 2. … |
Rules:
- Diagram and table counters are independent (
Diagram 1thenTable 1thenDiagram 2is valid) - Counters continue across shards via shared per-guide hook state
- Each guide starts at 1 for each kind (two guides sharing one insert file each get their own
Diagram 1) - Repeat links to an already inlined file do not consume a new number (back-link only)
inlineInserts deduplication
Within one guide:
- First link to
../diagrams/flow.md→ inline under numbered heading - Later links to the same resolved file (any path spelling, with or without
#fragment) →`[label](#diagram-1-…)` - Same basename in different libraries (
diagrams/overview.mdvstables/overview.md) → separate headings and anchors
inlineInserts path resolution
Lookup order for insert shard paths:
- Relative to the current shard directory
- Relative to the shard parent directory
process.cwd()and its parent- Optional
hooksConfig.inlineInserts.searchRoots
inlineInserts config
Runs by default. Optional search roots:
{
"name": "architecture-review",
"compile": {
"hooksConfig": {
"inlineInserts": { "searchRoots": ["diagrams"] }
}
}
}Opt out: "hooks": { "inlineInserts": false }. See Default compile hooks.
inlineInserts compile example
Shard input:
| Insert | Summary |
| ------------------------------------------- | ----------- |
| [Request flow](../diagrams/request-flow.md) | Client path |
See [Request flow](../diagrams/request-flow.md) again in prose.Compiled fragment (first guide mention):
| Insert | Summary |
| ------ | ------- |
|
#### Diagram 1. Request flow
| Step | Actor |
| ---- | ------ |
| 1 | Client |
| Client path |
See [Request flow](#diagram-1-request-flow) again in prose.Example fixture: examples/sample-guides/inserts-demo/. See GitHub media reference for a format matrix (PNG, JPEG, GIF, SVG, MP4, MP3/WAV, Mermaid, tables, lists) and minimal generated sample assets under figures/ and media/.
Figure with embedded image — shard figures/component-map.md:
Catalog link `[Component map](../figures/component-map.md)` compiles to a numbered #### Figure 1. … heading followed by that image markdown.
Media with embedded video — shard media/walkthrough.md:
<video src="./walkthrough.mp4" controls></video>Catalog link `[Walkthrough](../media/walkthrough.md)` compiles to #### Media 1. Walkthrough followed by the video embed.
Cross-guide link rewriting
Specification for assembly-time cross-shard and cross-guide link rewriting. Tests in packages/mdcp-core/test/cross-guide-links.test.ts map to the sections below (docs first, then TDD).
Multi-output consumer repos compile separate monoliths (for example glossary.md, architecture-review.md, technical-guide.md) from shards that span review/, security/, features/, and sibling guide directories. Source shards link with relative .md paths; compiled output must use stable in-document or cross-monolith #slug targets so link-fragment lint passes.
Cross-guide purpose
At compile time, MDCP:
- Builds a guide link index from every guide in
compileOrder— each manifest-listed shard maps to its compiled{guideName, outputBasename, slug}(slug from the demoted first heading, same rules as intra-guide rewrite), plus shards linked transitively from section bodies - Rewrites cross-guide
.mdlinks per shard (using the shard path for relative resolution) before sections are stitched - Rewrites publish-relative
../file links per shard when the guide hascompile.outputFile— see Publish-relative link rewriting - Rewrites same-guide
./section.mdlinks on the assembled body (intra-guide pass)
Cross-guide handles indexed markdown between guides. Publish-relative rebases remaining file paths for outputs outside the shard tree (no manual path config). This is an assembly-time pass, not a compile hook.
Cross-guide link matching
A link is rewritten when all of the following hold:
- Standard markdown link syntax:
[label](path) - Target path ends in
.md(optional#fragment) - Target is not
http://,https://, or#… - Target resolves to a shard registered in the guide link index
- Target shard's guide is not listed in
compile.crossGuideLinks.ignoreGuideson the compiling guide
Same-guide ./section.md links are handled by the intra-guide pass after assembly; cross-guide rewrite handles ../ and repo-scoped paths that point at another guide's shards.
Cross-guide resolution
Path lookup order (relative to the current shard directory):
- Relative to the shard directory (
dirname(sourceFile)) - Relative to the shard parent directory
compile.scopeRootwhen set on the compiling guideprocess.cwd()and its parent
When the resolved absolute path is in the guide link index:
| Case | Rewritten target |
| ------------------------------------------------ | ----------------------------------------------------------------------- |
| Same compiled output as the compiling guide | #slug or #fragment when the link includes a fragment |
| Different compiled output (compile.outputFile) | {outputBasename}#slug (for example architecture-review.md#find-004) |
| Monolith output (no per-guide outputFile) | #slug |
| Target guide in ignoreGuides | unchanged — keep source .md path (link to shard, not monolith) |
Finding shards (FIND-*.md) use the finding id from the filename (for example #find-004), not the parent outcomes section slug.
Cross-guide exclusions
The pass does not transform:
- External URLs
- Same-document
#fragmentlinks - Markdown links that do not resolve to an indexed shard
- Non-markdown paths (handled by
codeEvidenceor left unchanged) - Links to shards in guides listed in
compile.crossGuideLinks.ignoreGuides(publish-relative may still rebase the unchanged shard path for publish outputs — see Publish-relative link rewriting)
Cross-guide config
Minimal multi-output setup — index and rewrite run automatically from compileOrder and per-guide compile.outputFile:
{
"outputDir": "_build/compiled",
"compileOrder": ["glossary", "architecture-review", "technical-guide"],
"guides": [
{
"name": "glossary",
"path": "glossary",
"compile": {
"scopeRoot": ".",
"outputFile": "glossary.md"
}
},
{
"name": "architecture-review",
"path": "review",
"compile": {
"scopeRoot": ".",
"manifest": "shards.md",
"outputFile": "architecture-review.md"
}
},
{
"name": "technical-guide",
"path": "technical",
"compile": {
"scopeRoot": ".",
"outputFile": "technical-guide.md"
}
}
]
}compile.crossGuideLinks.ignoreGuides
Set on the guide being compiled. Guide names in this list keep source .md paths for cross-guide links instead of rewriting to that guide's monolith #slug target (ignoreGuides). Use when one compiled guide should link to live shard files for specific guides (for example technical reference docs that are not folded into a review bundle).
{
"name": "glossary",
"compile": {
"outputFile": "glossary.md",
"crossGuideLinks": {
"ignoreGuides": ["technical-guide"]
}
}
}Cross-guide compile example
Glossary shard input (glossary/terms.md):
See [FIND-004](../review/outcomes/FIND-004.md) in the architecture review.Review shard (review/outcomes/FIND-004.md):
# FIND-004 — Example finding
Body.Compiled glossary output:
See [FIND-004](architecture-review.md#find-004) in the architecture review.Cross-guide multi-target (three guides)
When one guide links to shards in two or more other guides, each link rewrites to that shard's own compile.outputFile independently.
Hub shard input (glossary/terms.md):
## Terms
See [FIND-004](../review/outcomes/FIND-004.md) and [Deployment](../technical/deployment.md).Compiled glossary.md (each target keeps its guide output):
## Terms
See [FIND-004](architecture-review.md#find-004) and [Deployment](technical-guide.md#deployment).Cross-guide ignore example (mixed monolith and shard links)
Same hub shard with ignoreGuides: ["technical-guide"] on the glossary guide:
## Terms
See [FIND-004](architecture-review.md#find-004) and [Deployment](../technical/deployment.md).Review targets use the compiled monolith; ignored guides keep shard paths. Tests in packages/mdcp-core/test/cross-guide-links.test.ts cover index entries, per-link routing, ignoreGuides, and end-to-end compile.
Publish-relative link rewriting
Specification for assembly-time rebasing of shard-relative file links when a guide publishes outside the shard tree. Tests in packages/mdcp-core/test/publish-links.test.ts and packages/mdcp-core/test/links.test.ts map to the sections below.
Why this pass exists
Shards are authored with paths relative to where the file lives in the guide tree:
../features/foo.mdfromdocs/developer/../../features/foo.mdfromdocs/client-core/compile-hooks/../../package.jsonfromdocs/developer/(repo root)
That works while readers open shards under docs/. It breaks when the same content compiles to a publish output elsewhere — for example DEVELOPERS.md at the repo root or packages/mdcp-cli/README.md.
Problem: a single stitched document no longer knows which shard each ../ hop came from, so post-stitch string substitution cannot reliably rebase paths. Nested shards use different ../ depth; publish targets sit at different locations (repo root, packages/*/).
Solution: resolve and rebase per shard, before stitch:
- Resolve the link from
dirname(sourceFile)to an absolute target path - Emit
relative(dirname(publishOutputFile), absoluteTarget)in the compiled body
No stripParentSegments, oneLevelPrefix, or other publish-path config — geometry comes from sourceFile, compile.outputFile, and the filesystem.
When it runs
| Condition | Publish-relative rewrite |
| ----------------------------------------------- | -------------------------------------------------------- |
| Guide has compile.outputFile set | Yes — per shard, after cross-guide rewrite |
| Guide outputs only to _build/{name}.md | No — shard-relative paths stay as authored |
| Optional monolith (outputFile at config root) | No for guides without their own compile.outputFile |
Implementation: rewritePublishRelativeLinks in packages/mdcp-core/src/compile/publish-links.ts, invoked from assembleGuide when publishOutputFile is set.
Division of labor (three link passes)
Assembly applies specialized passes instead of one generic rewriter:
| Pass | Scope | Input links | Output |
| -------------------- | --------------------------------------------- | ------------------------------------------ | ---------------------------------------------------- |
| Cross-guide | Indexed .md in another guide | ../other-guide/shard.md | {outputFile}#slug or unchanged when ignoreGuides |
| Publish-relative | Remaining ../ file links on publish outputs | ../features/foo.md, ../../package.json | Path relative to publish file |
| Intra-guide | Same-guide section shards | ./section.md | #anchor in assembled body |
Cross-guide runs first and uses the guide link index. Publish-relative handles everything else that still starts with ../ — config files, package.json, and shard paths left unchanged by ignoreGuides.
Publish-relative matching
A link is rewritten when all of the following hold:
- Standard markdown link syntax:
[label](path) - Target starts with one or more
../segments (not./— see exclusions) - Target is not
http://,https://,mailto:, or#… - Target resolves to an existing file from the shard directory (then guide directory, then
compile.scopeRoot) - Resolved path is not a same-guide indexed shard (intra-guide pass owns those)
- Resolved path is not another guide's
compile.outputFile(cross-guide already rebased)
Publish-relative resolution
Path lookup order (same as cross-guide shard resolution):
resolve(dirname(sourceFile), filePart)resolve(guideDir, filePart)compile.scopeRootwhen set
Then:
relative(dirname(publishOutputFile), resolvedAbsolute) + optional #fragmentpublishOutputFile is the absolute path from resolveGuideLinkBase for the guide's compile.outputFile.
Publish-relative exclusions
The pass does not transform:
- External URLs
- Same-document
#fragmentlinks ./section.mdand other./paths (cross-guide or intra-guide handle.md; publish-relative only matches../)- Links cross-guide already rewrote to
{otherPublishOutput}#slug - Unresolvable paths (left unchanged)
Repo dogfood examples
Config: docs/mdcp.config.json — developer, client-cli, and client-core use compile.outputFile.
developer → DEVELOPERS.md (repo root)
| Shard input (docs/developer/…) | Compiled in DEVELOPERS.md |
| -------------------------------- | ---------------------------------- |
| ../../package.json | package.json |
| ../features/feature-catalog.md | docs/features/feature-catalog.md |
| ../mdcp.config.json | docs/mdcp.config.json |
client-cli → packages/mdcp-cli/README.md
| Shard input (docs/client-cli/…) | Compiled in README |
| --------------------------------- | ---------------------------------------- |
| ../features/feature-catalog.md | ../../docs/features/feature-catalog.md |
client-core/compile-hooks/ → packages/mdcp-core/README.md
Nested shards use more ../ segments in source; per-shard resolution still yields the correct publish-relative path:
| Shard input | Compiled in README |
| -------------------------------------------------------------- | ------------------------------------------------------------------- |
| ../../features/design-constraints/preprocessor-templating.md | ../../docs/features/design-constraints/preprocessor-templating.md |
ignoreGuides interaction
When compile.crossGuideLinks.ignoreGuides keeps a cross-guide link as a shard .md path, publish-relative still rebases that path for the publish file. Example: client-cli with ignoreGuides: ["features"] compiles ../features/feature-catalog.md to ../../docs/features/feature-catalog.md in the package README.
Link validation accepts those shard paths when the target guide is listed in ignoreGuides on the compiling guide. See Link validation.
Related
- Cross-guide link rewriting — indexed
.mdbetween guides - Compile hooks — overview — assembly pipeline
- API — Config —
compile.outputFile - codeEvidence — separate path rebase for repo source evidence links
Related packages
| Package | Use |
| -------------------------------------------------------------------------------------- | ----------------------------- |
| @bwilliamson/mdcp-cli | mdcp command-line interface |
| @bwilliamson/mdcp-presets | Starter markdownlint configs |
Further reading
License
MIT
domain glossary
Per-repository glossary shards under docs/glossary/ for acronyms and product vocabulary. When legacy systems reuse the same term for different concepts, add a disambiguation entry and link from feature shards on first use. Start the glossary before large feature shards when migrating or onboarding new projects.
One term per shard
Each definition lives in its own .md file with a single # heading (the term). Link the term from feature shards on first use, for example [GFM](./gfm.md) or ../glossary/gfm.md from another guide.
Multiple index files
When a glossary grows beyond a comfortable manifest size, group entries in sub-index manifests:
| File | Role |
| ------------------- | ---------------------------------------------------------------------------------------- |
| index.md | Master index — preamble plus links to every term shard (required for cross-guide stitch) |
| index-protocol.md | Example sub-index — protocol-related terms only |
| index-format.md | Example sub-index — format and compile terms |
Stitched into other guides: link ../glossary/index.md from each guide index.md. Set compile.scopeRoot to glossary on those guides so transitive .md links from the glossary tree pull term shards into compile output without listing every term in the parent manifest.
Standalone glossary output: add glossary to compileOrder with compile.outputFile and optionally compile.manifest: index-protocol.md (or another sub-index) when you want a separate compiled glossary per group.
MDCP
MarkDown Context Protocol — a protocol for repository documentation context: sharded intent and design in Markdown, validated compile output for agents, CI, and human readers. The CLI is one surface; compile, check, refs lookup, and export --llm implement the shared context layer.
protocol version
Four-part version for MDCP artifact and config compatibility (default 0.4.0.0). Declared in mdcp.config.json as protocolVersion and in mdcp.v*.llms.txt as the first-line header mdcp-llms-index: 0.4.0.0. Filename may abbreviate trailing .0 segments (mdcp.v0.4.llms.txt ≡ 0.4.0.0).
Version history: 0.4.0.0 is the first published llms-index spec (open alpha). Pre-0.4 compile and doc-authoring evolution is recorded in package changelogs and the 0.4.0 changesets — see Versioning and releases.
Protocol version is not npm semver. npm @bwilliamson/[email protected] implements this draft protocol profile while tooling remains pre-1.0. valpha is the open-alpha symlink; vstable is reserved for npm 1.0.0.
mdcp-llms-index
Export profile for the versioned agent bootstrap file mdcp.v*.llms.txt in the docs root. Short index (~80–200 lines) describing how to adopt and query MDCP — not a full documentation dump. Read Vision and roadmap.
GFM
GitHub Flavored Markdown — standard Markdown plus GitHub extensions (tables, task lists, fenced code). Not Pandoc, LaTeX, or wikilinks.
Authored GFM
Shard markdown as written before compile — no preprocessor substitution or template conditionals. Compile hooks may transform it during assembly; read Preprocessor / templating (out of scope).
ignoreGuides
Guide names listed on the compiling guide under compile.crossGuideLinks.ignoreGuides. Cross-guide links to those guides keep source shard .md paths instead of rewriting to monolith #slug targets. Does not exclude the guide from compileOrder or the link index — only skips link rewrite for those targets. On publish outputs, publish-relative rewrite still rebases the shard path for the publish file. Read Cross-guide link rewriting.
