Host Docs
A host application often keeps its own documentation next to the code: a docs/wiki/ folder of generated markdown
pages, runbooks, conventions. ExposeDocs imports those documents into a Beacon project at startup
so MCP agents can search them and read them with get_documentation, alongside the host’s data
(Host DbContext). It works with UseAI = false: no LLM and no embedder is needed.
Quick start
Section titled “Quick start”var beacon = builder.Services.AddBeaconServices(builder.Configuration, o => { /* your options */ });
beacon.ExposeDbContext<HostContext>(o => o .ReadOnlyConnection("BeaconReadOnly") .AllowTables("Loan", "LoanInstallment", "Customer"));
beacon.ExposeDocs(o =>{ o.ProjectName = "HostContext"; // the project ExposeDbContext uses (its Name/ProjectName) o.FromDirectory("docs/wiki") // relative to ContentRootPath; default glob **/*.md .Exclude("drafts/**");});
beacon.UseSqlServer(); // ends the builder chain: call ExposeDocs first
var app = builder.Build();// ... apply migrations ...await app.Services.SyncBeaconHostAsync(); // syncs ExposeDbContext data sources, then ExposeDocs documentsSyncBeaconHostAsync is the single startup entrypoint for everything a host declares. It runs the
ExposeDbContext sync first (exactly what SyncBeaconHostDataSourcesAsync does; that call still works on its
own) and then imports the documents. It is a plain startup call, not a hosted service.
Both calls are safe to run from several replicas at once: the whole sync holds one lock in Beacon’s own database
(PostgreSQL pg_try_advisory_lock on a fixed key, SQL Server sp_getapplock with a session owner), so replicas
starting together run it one after another instead of racing each other’s inserts. A replica waits up to 5 minutes
for the lock. A unique-key race that still happens (a row created outside the lock) re-runs the idempotent sync once
instead of failing startup. Other providers run the sync unlocked and log a warning. A host that coordinates startup
another way can register its own IHostSyncLock.
The host project
Section titled “The host project”ExposeDbContext, ExposeDocs and AddHostEndpointTools all resolve their ProjectName through one shared
resolver, so they always land on the same project. The host project is identified by a host key
(Project.HostManagedKey = host:{ProjectName, trimmed and lower-cased}), never by its display name, which is neither
unique nor protected:
- On first sync the host project is created with the key. If a project that is not host-managed already uses the
name (for example one a Beacon user created by hand), it is left untouched and the host project is created as
"{ProjectName} (host)"(then"(host 2)", …), with a warning in the log. A user project with the same name never receives the host database, documents or endpoint tools. - Renaming the host project in the UI is fine: it is still found by its key.
- An archived host project is restored, because the host still declares it.
- Changing
ProjectNamecreates (or claims) the project for the new name, and the host data source is unlinked from every other project, including the project of the old name, so it loses access to the host database.
Shipping the documents
Section titled “Shipping the documents”The docs are not part of the build output by default, so the host has to ship them. Either copy them next to the app:
<!-- YourHost.csproj --><ItemGroup> <Content Include="..\docs\wiki\**\*.md" CopyToOutputDirectory="PreserveNewest" LinkBase="docs/wiki" /></ItemGroup>o.FromDirectory("docs/wiki");FromDirectory resolves a relative path against IHostEnvironment.ContentRootPath. Make sure that is the
output directory in your deployment (it is when the app is started from its own folder), or pass an absolute
path such as Path.Combine(AppContext.BaseDirectory, "docs/wiki").
Or embed them in the assembly:
<ItemGroup> <EmbeddedResource Include="..\docs\wiki\**\*.md" LogicalName="docs/wiki/%(RecursiveDir)%(Filename)%(Extension)" /></ItemGroup>o.FromEmbeddedResources(typeof(Program).Assembly, "docs/wiki/");The part of the resource name after the prefix is the document path (backslashes become /). Without a
LogicalName, MSBuild mangles folder names (40-web becomes _40_web) and joins them with dots, so set one.
Options
Section titled “Options”| Option | Meaning |
|---|---|
ProjectName | Required. The host project the documents attach to, found by its host key (see The host project); created when missing. |
FromDirectory(path, params globs) | Files under path (relative to the content root, or absolute) matching the globs. Default glob **/*.md. |
FromEmbeddedResources(assembly, prefix) | Manifest resources whose name starts with prefix. |
Exclude(params globs) | Skips documents whose relative path matches (applies to every source of this call). |
MaxDocumentBytes | Per-document cap, default 512 KB. Larger files are skipped with a warning that names only the path. |
ExposeDocs may be called more than once, for several folders or for several projects. Only .md,
.markdown and .txt files are ingested, whatever the glob matches. Globs must be relative and may not contain
..; a file that resolves outside the configured directory, and any symbolic link inside it (file or folder), is
rejected. A configured directory that does not exist fails startup, so a broken deployment cannot silently archive
every imported document.
What the sync does
Section titled “What the sync does”Each document is stored as a ProjectImportedDocument, keyed by project, source (docs:dir:docs/wiki,
docs:res:{Assembly}:{prefix}) and relative path.
- Title: the frontmatter
title, else the first#heading (outside code fences), else the file name. - Frontmatter: a leading
---YAML block is stripped from the body and kept as JSON (flat keys, quoted strings,[a, b]and- itemlists, such as asources:list). - Idempotent: a document whose content hash is unchanged is left alone; nothing is written when nothing changed. A changed document is updated in place, a removed file is archived, and a file that comes back is un-archived.
- Case-insensitive paths: source and path are matched ignoring letter case, as SQL Server’s unique index
compares them. Renaming
Guide.mdtoguide.mdupdates the existing row (it takes the new casing). Two files whose paths differ only by case (possible on a case-sensitive file system or in embedded resources) cannot both be stored: the first is imported and the other is skipped with a warning that names its path. - Chunked for search: new and changed documents are split into sentence-window chunks (the same chunker and
DocChunkWindowSentences/DocChunkOverlapSentencessettings as generated documentation). Keyword search works on them immediately. When the local embedder and semantic retrieval are enabled, the sync then embeds the new chunks through the doc-chunk re-index, and the recurringmcp-docchunk-reindexjob keeps them current. An embedding failure is logged and does not fail startup.
The documents belong to one project and are only ever visible to callers authorized for that project.
On the MCP surface
Section titled “On the MCP surface”get_documentation: the project-level response (no arguments) ends with an Imported documents listing (title + path).document: "<path>"(or an exact title) returns one document in full; a title shared by several documents is an error that names their paths.search: imported documents match on title, path or chunk text and appear as- **[IMPORTED_DOC]** \path` (title), followed by— snippet` when there is one, with or without embeddings.get_contextwithformat: "agents_md": a deterministic project brief in markdown to drop into an agent workspace’sAGENTS.md: the project description, data sources (host-managed ones marked read-only), up to 30 key tables (best-described and most-related first), masked columns, up to 30 glossary terms, the imported document titles, and how to use Beacon’s tools (get_query_context→dry_run→query, read-only, masked columns stay masked). Built from the catalog only, with no LLM call, and audited like every tool call.
The REST API exposes the same documents: GET /beacon/api/projects/{id}/imported-documents (listing) and
GET /beacon/api/projects/{id}/imported-documents/{documentId} (one document with its content). Both need Read in
the project under project access, for sessions and API keys alike; a
project the caller cannot read answers 404.
Example: data and docs in one project
Section titled “Example: data and docs in one project”beacon.ExposeDbContext<HostContext>(o =>{ o.Name = "BackOffice"; o.ProjectName = "Back Office"; o.ReadOnlyConnection("BeaconReadOnly").AllowTables("Loan", "LoanInstallment", "Merchant", "Customer");});
beacon.ExposeDocs(o =>{ o.ProjectName = "Back Office"; o.FromDirectory("docs/wiki");});With the Content Include item above, the pages land in docs/wiki/ next to the host’s assembly. On each start only
the pages that changed since the last deployment are re-chunked.