Skip to content

Host Docs

View .md

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.

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 documents

SyncBeaconHostAsync 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.

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 ProjectName creates (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.

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.

OptionMeaning
ProjectNameRequired. 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).
MaxDocumentBytesPer-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.

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 - item lists, such as a sources: 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.md to guide.md updates 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 / DocChunkOverlapSentences settings 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 recurring mcp-docchunk-reindex job 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.

  • 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_context with format: "agents_md": a deterministic project brief in markdown to drop into an agent workspace’s AGENTS.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.

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.