Skip to content
Menu

Extending your app

Explore · 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
01Your DbContext as a data source

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

Program.cs
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
Stored{"hostConnectionStringName":"ShopReadOnly"}
Not storedServer=sql.acme.example;User Id=beacon_ro;Password=…

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.

DATA SOURCE · SHOPDBCONTEXTHost-managed, read-only
dbo.OrdersAllowTables
dbo.OrderLinesAllowTables
dbo.CustomersAllowTables
dbo.ProductsAllowEntity<Product>
dbo.AuditLognot allow-listed: invisible
Columns of dbo.Customers
Idint, PKExposed
Namenvarchar(200)Exposed
Emailnvarchar(320)Masked
Phonenvarchar(40)Masked
NotesExcludeColumnsExcluded
PasswordHashsecret-like nameExcluded (secret-like name)

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
Runs

Every table is allow-listed and schema-qualified. Email is a direct projection, so it is returned masked.

IdNameEmail
11904Ivan Kovač***

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.

02Your docs as grounding

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.

Program.cs, on the same builder
.ExposeDocs(x =>
{
x.ProjectName = "Shop";
x.FromDirectory("docs", "**/*.md").Exclude("drafts/**");
// x.FromEmbeddedResources(typeof(Program).Assembly, "Shop.Docs.");
// x.MaxDocumentBytes = 512 * 1024;
})
DOCS/at startup
orders/
statuses.mdImported
refunds.mdImported
support/
escalation.mdImported
drafts/
returns-v2.mdExclude("drafts/**")
exports/
full-catalogue.md · 2.1 MBOver MaxDocumentBytes
MCP · search and get_documentation
→ 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.

03Your endpoints as MCP tools

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.

Minimal API
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);
Or a controller action
[HttpGet("{customerId:int}/orders")]
[Authorize(Policy = "ViewOrders")]
[BeaconTool("customer_orders", Description = "Orders of one customer, newest first.", ReadOnly = true)]
public IActionResult CustomerOrders(int customerId) { /* … */ }
MCP TOOLread-only
api_order_by_id

One order by id.

Route
GET /api/orders/{id:int}
Argument
id · route value, required
Policy
ViewOrders
Project
Shop (host project)
readOnlyHint: truedestructiveHint: falseidempotentHint: trueopenWorldHint: false

How a call is dispatched

  1. Check the callerA mapped Entra caller of the host project, with valid arguments.
  2. Build a principalIMcpHostPrincipalFactory builds the host identity for the caller.
  3. New requestA new DI scope and HttpContext. No Authorization header or cookies are forwarded.
  4. AuthorizeThe endpoint’s own policy, evaluated against the host principal.
  5. Run the endpointDispatch middleware, then binding, filters and the result.
  6. 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.

12 ENDPOINTSnamed tools
api_order_by_idapi_customer_ordersapi_refund_by_idapi_shipment_status+ 8 more
34 ENDPOINTScatalog
search_apikeywords → matching tools with their input schemascall_apia name plus arguments
04Startup

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.

  1. RegisterExposeDbContext and ExposeDocs on the Beacon builder.
  2. End the chainUseSqlServer or UsePostgreSql last: it returns IServiceCollection.
  3. Add toolsAddBeaconMcp().AddHostEndpointTools for the same project.
  4. MigrateUseBeacon creates the schema and applies migrations.
  5. SyncSyncBeaconHostAsync: data sources first, then documents.
  6. ServeEndpoint tools appear once the host project exists.
Program.cs
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 lock

Idempotent

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.

05Worked example

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

SUPPORT ASSISTANTShop
What is the status of order 104 236 and who is the customer?
api_order_by_id200
arguments{ "id": 104236 }
result{ "id": 104236, "status": "Shipped",
  "customerId": 11904, "total": 48.50,
  "placedAt": "2026-10-12T09:14:00Z" }
query1 row
datasource_nameShopDbContext
sqlSELECT c.Id, c.Name, c.Email, c.Phone
FROM dbo.Customers c
WHERE c.Id = 11904
IdNameEmailPhone
11904Ivan Kovač******
Order 104 236 is Shipped. It was placed on 12 October 2026 for 48.50. The customer is Ivan Kovač (customer 11 904). Their email and phone are masked by the application's data policy, so I cannot show them.
  1. 1
    The 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 ViewOrders policy and runs GET /api/orders/104236 in-process. The JSON comes back as text and structured content.

  2. 2
    The query passes the host policy

    dbo.Customers is 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.

  3. 3
    Both 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.

    ToolCallerOutcome
    api_order_by_iduserSuccess
    queryuserSuccess
06Saved queries as tools

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.

Query · Orders by status
-- Step 1 · ShopDbContext (host data source)
SELECT o.Id, o.Status, o.PlacedAt, o.Total
FROM dbo.Orders o
WHERE o.Status = {status}
AND o.PlacedAt >= {placed_from}
MCP TOOLVersion 4 approved
Expose as MCP tool
Name
orders_by_status
Description
Orders in one status placed since a date.
Calls run
Version 4, its approved snapshot
WHAT THE AGENT SEEStools/list
q_orders_by_status
status
string, required
placed_from
ISO 8601 string, required
readOnlyHint: trueidempotentHint: true
Version stateCallable
Active, its approval request approvedYes
Pending approval (a later edit)Keeps running the approved one
Draft, rejected or without an approvalNo

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.

07Replaceable services

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.

InterfaceDefaultReplace it to
IBeaconSchedulerNone (required)Use your job runner.
IEmailAdapternoneSend email (AddEmailAdapter<T>()).
IBeaconAuthenticationProvideruser storeCheck passwords somewhere else.
IBeaconAuthorizationProvideruser store / allow-allCap users’ global read/write from your own model.
IProjectMembershipSourceBeacon’s membership tableTake project membership from your application.
IMcpCallerMapperBeacon:Mcp:CallersMap JWT callers to projects and scopes your own way.
IMcpHostPrincipalFactorycopies name, email, oidBuild the principal your endpoints expect for MCP calls.
IHostEndpointDispatchMiddlewarenoneRun tenant, culture or audit logic around dispatched tool calls.
IApprovalNotifierSignalRNotify approvers elsewhere.
IBeaconUserContextHttpContextUserContextResolve the current user differently.

With IProjectMembershipSource replaced, Beacon's membership endpoints answer 409. IBeaconUserContext is registered in two places; replace it in both.