Extending your app
Teach Beacon your application.
An embedded Beacon can know your application, not only your databases. Expose your EF Core model as a read-only data source, import your Markdown docs as grounding, and turn selected HTTP endpoints into MCP tools. All three are opt-in and land in one host project.
- 3host extensions
- 1startup sync call
- 0host connection strings stored
ExposeDbContextExposeDocsAddHostEndpointToolsYour EF Core model, read-only, with a default-deny allow-list.
ExposeDbContext turns the model you already have into a Beacon data source. Metadata comes from the model without opening a connection, only allow-listed tables are visible, and the policy is checked again on every statement.
builder.Services.AddBeaconServices(builder.Configuration, options => { /* … */ }) .ExposeDbContext<ShopDbContext>(x => { x.ProjectName = "Shop"; // the host project x.ReadOnlyConnection("ShopReadOnly") // ConnectionStrings:ShopReadOnly, a read-only login .AllowTables("Orders", "OrderLines", "Customers") .AllowEntity<Product>() .ExcludeColumns(y => y.Table == "Customers" && y.Name == "Notes") .MaskColumns(y => y.Name is "Email" or "Phone"); }) .AddSqlServerConnector() .UseSqlServer(); // ends the chain: ExposeDbContext comes first{"hostConnectionStringName":"ShopReadOnly"}Beacon keeps only the encrypted name of the connection string. The real string is read from configuration whenever a connection is opened, and should belong to a login that can only SELECT the allow-listed tables.
Masked columns stay visible to the assistant, flagged [PII: values are masked in query results]. Names that look like secrets (password, token, hash, otp …) are excluded unless lifted with IncludeSecretLikeColumn.
Every statement is checked against the exposed slice
The check runs in the execution gate and again in the database provider, in the data source's own dialect. Choose a statement to see what the host policy decides.
SELECT c.Id, c.Name, c.Email FROM dbo.Customers c WHERE c.Id = 11904
Every table is allow-listed and schema-qualified. Email is a direct projection, so it is returned masked.
| Id | Name | |
|---|---|---|
| 11904 | Ivan Kovač | *** |
SELECT Name FROM Customers
Qualify the table: dbo.Customers. The connection’s default schema may differ from the model’s, so every table must be schema-qualified.
SELECT * FROM dbo.Customers
SELECT * and t.* are rejected with a list of the exposed columns to use instead.
SELECT c.Name FROM dbo.Customers c WHERE c.Email LIKE '%@acme.example'
A masked column may appear only as a direct projection, in COUNT(col), or in IS NULL / IS NOT NULL, so masked values cannot be probed.
SELECT c.Notes FROM dbo.Customers c
Excluded columns are invisible in metadata and rejected anywhere in a statement.
Host-masked values come back as *** in every result (NULL stays NULL), masked by output position whatever the column is called. SQL Server and PostgreSQL models are supported.
Your Markdown documentation, searchable by every assistant.
ExposeDocs imports the documents you ship with the application into the host project. They are chunked for keyword search at once, embedded when semantic retrieval is on, and visible only to callers authorized for that project.
.ExposeDocs(x => { x.ProjectName = "Shop"; x.FromDirectory("docs", "**/*.md").Exclude("drafts/**"); // x.FromEmbeddedResources(typeof(Program).Assembly, "Shop.Docs."); // x.MaxDocumentBytes = 512 * 1024; })→ search { "query": "when is an order shipped" } - **[IMPORTED_DOC]** `orders/statuses.md` (Order statuses) -- An order moves to Shipped when the carrier confirms the pickup … - **[IMPORTED_DOC]** `support/escalation.md` (Escalating a delayed order) → get_documentation { "document": "orders/statuses.md" } # Order statuses Every order is in exactly one status: Placed, Paid, Shipped, Delivered or Cancelled. An order moves to Shipped when …
Idempotent
An unchanged document is left alone. A changed one is updated in place, a removed file is archived, a returning file is restored.
No LLM needed
Keyword search works with UseAI = false. Title comes from the frontmatter, the first heading or the file name.
Only .md, .markdown and .txt files are ingested. get_context with format: "agents_md" lists the imported titles in its project brief.
Endpoints you mark become tools, called through your own pipeline.
AddHostEndpointTools turns the endpoints you mark into MCP tools named api_<name>. Each call runs in-process through the endpoint's model binding, filters and authorization policies, as a host identity built for the MCP caller.
builder.Services.AddBeaconMcp() .AddHostEndpointTools(x => { x.ProjectName = "Shop"; // the host project the tools belong to // x.AllowWriteTools = true; // off by default });
app.MapGet("/api/orders/{id:int}", (int id, OrderService orders) => orders.Find(id)) .RequireAuthorization("ViewOrders") .WithBeaconTool("order_by_id", "One order by id.", readOnly: true);[HttpGet("{customerId:int}/orders")][Authorize(Policy = "ViewOrders")][BeaconTool("customer_orders", Description = "Orders of one customer, newest first.", ReadOnly = true)]public IActionResult CustomerOrders(int customerId) { /* … */ }One order by id.
- Route
- GET /api/orders/{id:int}
- Argument
- id · route value, required
- Policy
- ViewOrders
- Project
- Shop (host project)
How a call is dispatched
- Check the callerA mapped Entra caller of the host project, with valid arguments.
- Build a principalIMcpHostPrincipalFactory builds the host identity for the caller.
- New requestA new DI scope and HttpContext. No Authorization header or cookies are forwarded.
- AuthorizeThe endpoint’s own policy, evaluated against the host principal.
- Run the endpointDispatch middleware, then binding, filters and the result.
- Capture and auditThe response, capped at MaxResponseBytes, audited as api_<name>.
ReadOnly is declared
Every tool states ReadOnly. Beacon never infers it from the HTTP verb, because back offices read over POST. A missing flag fails startup.
Authorization metadata required
[Authorize], a policy or [AllowAnonymous], or a host fallback policy. Otherwise startup refuses the endpoint. A permission filter is not metadata.
Entra callers only
Only mapped Entra callers (users or systems) of the host project that may read it see or call the tools. API-key callers cannot.
Runs as a host principal
The default factory gives a user no host permissions and never copies Entra roles or groups. Map users to your permissions in your own IMcpHostPrincipalFactory.
Write tools are opt-in
ReadOnly = false needs AllowWriteTools, is offered to user callers only, carries readOnlyHint = false so clients ask first, and is audited as api_<name>:write.
A catalog above 20 tools
Up to NamedToolLimit (20), each endpoint is its own tool. Above it, clients get search_api and call_api instead.
One host project, one startup call.
All three extensions resolve the same host project, found by its host key rather than its display name. Register them before the chain ends, migrate Beacon's schema, then sync.
- RegisterExposeDbContext and ExposeDocs on the Beacon builder.
- End the chainUseSqlServer or UsePostgreSql last: it returns IServiceCollection.
- Add toolsAddBeaconMcp().AddHostEndpointTools for the same project.
- MigrateUseBeacon creates the schema and applies migrations.
- SyncSyncBeaconHostAsync: data sources first, then documents.
- ServeEndpoint tools appear once the host project exists.
builder.Services.AddBeaconServices(builder.Configuration, options => { /* … */ }) .ExposeDbContext<ShopDbContext>(x => { /* allow-list, exclusions, masks */ }) .ExposeDocs(x => { /* docs folder */ }) .AddSqlServerConnector() .UseSqlServer(); // ends the chain: host extensions come first
builder.Services.AddBeaconMcp() .AddHostEndpointTools(x => x.ProjectName = "Shop");
var app = builder.Build();Beacon.Core.ServiceConfiguration.UseBeacon(app.Services, createSchema: true);await app.Services.SyncBeaconHostAsync(); // data sources first, then docs; idempotent; replica-safe lockIdempotent
The first run creates the project, the data source and the link. Later runs rewrite metadata only when the hash of the exposed model slice changed, and re-chunk only the documents that changed.
Safe on several replicas
The sync holds one lock in Beacon's database (pg_try_advisory_lock on PostgreSQL, sp_getapplock on SQL Server). Replicas starting together run it one after another, waiting up to 5 minutes.
Found by its host key
The host project carries host:shop. A user project already named "Shop" is left untouched, and the host project is created as "Shop (host)".
SyncBeaconHostDataSourcesAsync() syncs the data sources only, for hosts that do not ship documents.
A support assistant answers an order question.
A support agent signed in with Entra asks their assistant about one order. The assistant uses the host endpoint for the order and a query on the host data source for the customer. The host's own principal factory maps the agent to the ViewOrders permission, and the agent's Beacon user is an Editor in the project, which the query tool needs (a provisioned user starts with the default Viewer role).
arguments{ "id": 104236 } result{ "id": 104236, "status": "Shipped", "customerId": 11904, "total": 48.50, "placedAt": "2026-10-12T09:14:00Z" }
datasource_nameShopDbContext sqlSELECT c.Id, c.Name, c.Email, c.Phone FROM dbo.Customers c WHERE c.Id = 11904
| Id | Name | Phone | |
|---|---|---|---|
| 11904 | Ivan Kovač | *** | *** |
- 1The endpoint runs as the agent
Beacon checks that the caller is a mapped Entra user of the Shop project, builds the host principal, evaluates the
ViewOrderspolicy and runsGET /api/orders/104236in-process. The JSON comes back as text and structured content. - 2The query passes the host policy
dbo.Customersis allow-listed and schema-qualified, every column is exposed, and Email and Phone are direct projections, so the statement runs on the read-only login and both values are masked. - 3Both calls are audited
Each call writes an MCP audit row with the tool, the project, the caller kind and the outcome. For the endpoint call, argument values and response bodies are never written to logs.
Tool Caller Outcome api_order_by_id user Success query user Success
Reviewed SQL as a tool, without writing code.
A SecurityAdmin (or an Admin) can publish a saved query as an MCP tool named q_<name> from the query's page. The tool runs only an approved version, with the caller's arguments bound as database parameters.
-- Step 1 · ShopDbContext (host data source)SELECT o.Id, o.Status, o.PlacedAt, o.TotalFROM dbo.Orders oWHERE o.Status = {status} AND o.PlacedAt >= {placed_from}- Name
- orders_by_status
- Description
- Orders in one status placed since a date.
- Calls run
- Version 4, its approved snapshot
- status
- string, required
- placed_from
- ISO 8601 string, required
| Version state | Callable |
|---|---|
| Active, its approval request approved | Yes |
| Pending approval (a later edit) | Keeps running the approved one |
| Draft, rejected or without an approval | No |
Each {placeholder} becomes a parameter (@p0, @p1); argument text never reaches the SQL. On a host data source the allow-list and masks apply as for any other statement. Enable the approval workflow on hosts that use saved-query tools. Above 25 tools, callers get search_saved_queries and run_saved_query.
Swap a default by registering yours first.
Beacon registers most services with TryAdd, so your registration wins when it comes before AddBeaconServices, AddBeaconApiServices or AddHostEndpointTools, depending on who registers it.
| Interface | Default | Replace it to |
|---|---|---|
| IBeaconScheduler | None (required) | Use your job runner. |
| IEmailAdapter | none | Send email (AddEmailAdapter<T>()). |
| IBeaconAuthenticationProvider | user store | Check passwords somewhere else. |
| IBeaconAuthorizationProvider | user store / allow-all | Cap users’ global read/write from your own model. |
| IProjectMembershipSource | Beacon’s membership table | Take project membership from your application. |
| IMcpCallerMapper | Beacon:Mcp:Callers | Map JWT callers to projects and scopes your own way. |
| IMcpHostPrincipalFactory | copies name, email, oid | Build the principal your endpoints expect for MCP calls. |
| IHostEndpointDispatchMiddleware | none | Run tenant, culture or audit logic around dispatched tool calls. |
| IApprovalNotifier | SignalR | Notify approvers elsewhere. |
| IBeaconUserContext | HttpContextUserContext | Resolve the current user differently. |
With IProjectMembershipSource replaced, Beacon's membership endpoints answer 409. IBeaconUserContext is registered in two places; replace it in both.