Skip to main content
Glama
business-central-mcp

business-central-mcp

by SShadowS

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
PORTNoHTTP transport port (stdio transport ignores this).3000
LOG_DIRNoDirectory for log files../logs
LOG_LEVELNodebug / info / warn / error.info
STATE_DIRNoDirectory for session state../.state
BC_PROFILENoProfile id, e.g. BUSINESS MANAGER. Affects which Role Center loads and which pages Tell Me indexes.server default
BC_BASE_URLYesBC server base URL, e.g. http://your-bc-server/BC
BC_PASSWORDYesNavUserPassword password
BC_USERNAMEYesNavUserPassword username
BC_TENANT_IDNoMulti-tenant deployments only.default
BC_CLIENT_VERSIONNoVersion reported to BC during session open.27.0.0.0
BC_INVOKE_TIMEOUTNoPer-invoke timeout in ms. Kills hung sessions.30000
BC_RECONNECT_BASE_DELAYNoBase delay (ms) for exponential reconnect backoff.1000
BC_RECONNECT_MAX_RETRIESNoReconnect attempts after session death.4

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
extensions
{
  "io.modelcontextprotocol/skills": {
    "directoryRead": false
  }
}

Tools

Functions exposed to the LLM to take actions

NameDescription
bc_open_pageA

Opens a Business Central page by its numeric page ID and returns its complete state as a list of sections. Each section has a sectionId, kind (header / lines / factbox / subpage / requestPage), caption, and the appropriate content shape. Card-shape sections (most headers, factboxes, requestPages) carry fields[] (and headers also carry actions[]). List-shape sections (lines, list-bodied headers, repeater subpages) carry rows[] and totalRowCount. The header section adapts to its page: it is card-shape on Card pages and list-shape on List pages -- the kind stays "header" either way for path stability. This is the entry point for interactive, page-scoped work -- it returns a pageContextId that the page-scoped tools (bc_read_data, bc_write_data, bc_execute_action, bc_navigate, bc_respond_dialog, bc_close_page, bc_lookup) take as input, plus a stateVersion you can pass as expectedStateVersion to bc_write_data / bc_execute_action to guard against stale state. (bc_query, bc_run_report, bc_search_pages, bc_list_companies, and bc_switch_company do NOT need a pageContextId.) For bulk, read-only data over standard entities, prefer bc_query -- it needs no open page. Use bc_search_pages first if you do not know the page ID for an entity.

Card pages (single-record views like Customer Card=21) return one header (card-shape) plus any FactBox sections attached to the page. List pages (Customer List=22) return a header (list-shape, rows[] populated). Document pages (Sales Order=42) return a header (card-shape), a "lines" list-shape section with the document lines, and any FactBoxes.

Option/enum fields and boolean fields in card-shape sections carry two extra properties: "options" (the allowed choices as [{text, value}]) and "selectedOption" (the currently chosen entry). Always use the "value" string from "options" as the SaveValue payload when writing an enum field -- do NOT guess or invent values. Example: Item Card "Type" field returns options=[{text:"Inventory",value:"0"},{text:"Service",value:"1"},{text:"Non-Inventory",value:"2"}] and selectedOption={text:"Inventory",value:"0"}.

Typical workflow: bc_open_page -> bc_read_data (refresh / filter / paginate a section) -> bc_write_data (edit fields in any section) -> bc_execute_action (post / release / delete) -> bc_close_page. Always call bc_close_page when done. Do NOT call this if the page is already open -- reuse the existing pageContextId. On BC Online the first call opens a local Microsoft sign-in window (password and Authenticator stay in that window; never pass them as tool arguments). Retry after completing sign-in.

Optional bookmark parameter opens a Card page to a specific record. Bookmarks come from list rows in any prior section.

Examples:

  • { "pageId": 22 } opens Customer List. Sections: [{ "sectionId": "header", "kind": "header", "rows": [...], "actions": [...] }] (no fields[] on a list-shape header).

  • { "pageId": 21, "bookmark": "..." } opens Customer Card. Sections include the header card plus FactBoxes (e.g. { "sectionId": "factbox:Customer Statistics", "kind": "factbox", "fields": [...] }).

bc_read_dataA

Refreshes a single section on an already-open page. Returns { section: { sectionId, kind, caption, fields?, rows?, actions?, totalRowCount? }, stateVersion }. Card-shape sections (header, factbox, requestPage) refresh their fields[]; list-shape sections refresh rows[]. The returned stateVersion can be passed as expectedStateVersion to bc_write_data / bc_execute_action to reject stale-state writes. Requires a pageContextId from a prior bc_open_page call.

Do NOT use this for bulk or analytical reads over standard entities (customers, items, ledger entries, ...) -- prefer bc_query, which reads server-side via OData with no open page and no UI paging. Use bc_read_data when you need the interactive page's exact rows, factboxes, or option metadata.

Pass section: "header" (default) to refresh the page's header. Pass section: "lines" to refresh document line items. Pass a factbox sectionId (e.g. "factbox:Customer Statistics", as listed in the bc_open_page response) to refresh the FactBox card.

Option/enum and boolean fields in card-shape sections carry "options" (allowed choices as [{text, value}]) and "selectedOption" (current choice). When writing an enum field with bc_write_data, use the "value" string from "options" -- do NOT guess values. Example: after opening Item Card, the "Type" field returns options=[{text:"Inventory",value:"0"},{text:"Service",value:"1"},{text:"Non-Inventory",value:"2"}]; to change to Service, write value "1".

Filtering applies to list-shape sections only. Pass an array of { column, value }; values use BC filter syntax (exact "10000", ranges "10000..20000", wildcards "consulting", expressions ">1000"). Multiple filters combine with AND. Filters stay on the page until clearFilters (an empty value is refused: BC cannot remove a filter that way); bc_close_page clears them, so the page opens unfiltered next time.

clearFilters: true resets agent-applied filters and restores the page to its default/native filtered state before reading. Note: page-defined SourceTableView filters (set in AL code) remain active -- this does NOT guarantee a completely empty filter set. Use before applying new filters to avoid stacking. Applies to list-shape sections only. Runs before any filters[] in the same call.

Sorting: pass sort: { column, direction } to sort the repeater before reading. Applied server-side after any filters. Resets BC viewport to top of sorted result. "asc" = A-Z / 0-9, "desc" = Z-A / 9-0. The column must be a visible repeater column on the section. Non-sortable columns (FlowFields, BLOBs) may be rejected by BC with an error. Applies to list-shape sections only.

Column selection: pass columns: ["No.", "Name"] to limit the cells in each row, or the fields[] entries on a card section.

Range slicing: { offset, limit } returns rows[offset..offset+limit] for list sections. Use with totalRowCount for pagination.

Examples:

  • Refresh header: { "pageContextId": "abc" }

  • Filter customer list: { "pageContextId": "abc", "filters": [{ "column": "City", "value": "London" }] }

  • Sort by Name ascending: { "pageContextId": "abc", "sort": { "column": "Name", "direction": "asc" } }

  • Sort by Name descending: { "pageContextId": "abc", "sort": { "column": "Name", "direction": "desc" } }

  • Filter and sort: { "pageContextId": "abc", "filters": [{ "column": "City", "value": "London" }], "sort": { "column": "Name", "direction": "asc" } }

  • Clear filters and re-read: { "pageContextId": "abc", "clearFilters": true }

  • Clear and re-filter: { "pageContextId": "abc", "clearFilters": true, "filters": [{ "column": "City", "value": "London" }] }

  • Read sales order lines: { "pageContextId": "abc", "section": "lines" }

  • Refresh a FactBox: { "pageContextId": "abc", "section": "factbox:Customer Statistics" }

bc_write_dataA

Writes one or more field values on an already-open Business Central page. Pass a fields object keyed by the field names a read lists (the caption, or the AL control name of a caption-less control such as a wizard input) and string values. BC validates each field and returns the server-confirmed value, which may differ from input due to formatting, auto-completion, or lookups (e.g., entering a partial customer name resolves to the full match). Requires a pageContextId from bc_open_page.

A field (or list column) that BC publishes as hidden, disabled or not editable is not written: that result has success:false and an error saying why (a hidden list column is reported as hidden), and nothing is sent for it, because BC silently ignores such a write. Other fields in the same call are still written. A result with echoed:false and a warning means BC raised no error but sent no stored value back (BC echoes only a changed value, so rewriting the current value looks the same): the write is not confirmed, read the field back to verify. Write related fields together in one call (e.g., quantity and unit price), but avoid writing unrelated groups together because BC validation cascades may change dependent fields in unexpected order. Check the returned confirmed values to see what BC actually stored.

For Document page line items (Sales Order lines, Purchase Order lines), specify section: "lines" to write to the lines repeater. Use rowIndex (0-based row position) or bookmark (stable row identifier from bc_read_data results) to target a specific line. Prefer bookmark over rowIndex when rows may have been reordered or inserted since the last read.

Pass expectedStateVersion (from a prior bc_read_data or bc_open_page stateVersion field) to guard against acting on drifted state. If the page has been mutated by async events or a sibling operation since that read, the call is immediately rejected with code STALE_CONTEXT before touching BC. Re-read with bc_read_data to get the current stateVersion, then retry. Omit expectedStateVersion to skip the check.

Do NOT use this for triggering actions like Post, Delete, or Release -- use bc_execute_action instead. Do NOT use this for navigating to records -- use bc_navigate instead.

Examples:

  • Write to Card header: { "pageContextId": "abc", "fields": { "Name": "Contoso Ltd", "Address": "123 Main St" } }

  • Write to Sales Order line: { "pageContextId": "abc", "section": "lines", "rowIndex": 0, "fields": { "Quantity": "5", "Unit Price": "100" } }

  • Write with bookmark targeting: { "pageContextId": "abc", "section": "lines", "bookmark": "XXXX", "fields": { "Description": "Consulting Services" } }

  • Write with staleness guard: { "pageContextId": "abc", "fields": { "Name": "Contoso" }, "expectedStateVersion": 3 }

bc_execute_actionA

Executes either a named action OR a cue-tile drill-down on an open page. Pass action for header / line / system actions (Post, Delete, New, Release). Pass cue for Role Center cue tiles to open the underlying list (e.g. cue: "Sales Quotes" with section: "subpage:Activities" opens the Sales Quotes list). Requires a pageContextId from bc_open_page.

For cue drill-down, also pass section pointing at the subpage that owns the cuegroup. The returned openedPages array contains the targetPageContextId of the newly-opened list page.

For a named action: validates the action is enabled, sends the InvokeAction RPC, applies the resulting events, and returns updatedFields / changedSections / dialogsOpened / openedPages.

Use exactly one of "action" or "cue" -- passing both is an error.

If the action triggers a confirmation dialog or modal page, the response includes a dialogsOpened array with the dialog's formId and details. When requiresDialogResponse is true, you must follow up with bc_respond_dialog to confirm or cancel. Each dialogsOpened entry carries its own pageContextId: when the dialog has input fields (listed in its fields array), fill them with bc_write_data against that pageContextId before pressing OK with bc_respond_dialog. Texts the action shows through AL Message() (e.g. "3 documents were sent") come back in messages: [{ text, type }].

Row-scoped actions (Delete, Edit on a list row) require targeting a specific row. Use rowIndex (0-based) or bookmark to specify which row the action applies to. For Document pages, use section to disambiguate between header and line actions (e.g., "Delete" on header deletes the whole document, "Delete" on "lines" deletes one line).

For batch operations across multiple rows (e.g. deleting several lines at once), pass bookmarks (an array) instead of bookmark/rowIndex -- the first entry is the anchor row. Only actions that consume a selection (e.g. Delete) act on all listed rows; current-row-only actions (Edit, View, DrillDown, New) reject bookmarks[] and must use bookmark/rowIndex instead. bookmarks[] is mutually exclusive with bookmark, rowIndex, and cue.

Pass expectedStateVersion (from a prior bc_read_data or bc_open_page stateVersion field) to guard against acting on drifted state. If the page has been mutated by async events or a sibling operation since that read, the call is immediately rejected with code STALE_CONTEXT before touching BC. Re-read with bc_read_data to get the current stateVersion, then retry. Omit expectedStateVersion to skip the check.

Do NOT use this for writing field values -- use bc_write_data. Do NOT use this to open records from a list -- use bc_navigate with drill_down action instead.

Examples:

  • Drill into a cue tile: { "pageContextId": "rc1", "section": "subpage:Activities", "cue": "Sales Quotes" }

  • Post a sales order: { "pageContextId": "so1", "action": "Post" }

  • Delete a row: { "pageContextId": "list1", "action": "Delete", "bookmark": "..." }

  • Create new record: { "pageContextId": "abc", "action": "New" }

  • Delete a document line: { "pageContextId": "abc", "action": "Delete", "section": "lines", "rowIndex": 2 }

  • Delete multiple rows in one call: { "pageContextId": "list1", "action": "Delete", "bookmarks": ["bk1", "bk2"] } -- only selection-consuming actions like Delete act on all listed rows

  • Execute with staleness guard: { "pageContextId": "abc", "action": "Post", "expectedStateVersion": 5 }

If the action produces a file (Open in Excel, Print, export), its bytes appear in downloads[] (an operationId replay returns the metadata with error code NOT_REPLAYED instead of the bytes); links BC would open externally appear in externalUris[] and are never fetched by the server.

bc_close_pageA

Closes an open Business Central page and frees its server-side resources including the WebSocket form session. Always call this when you are finished working with a page to prevent resource leaks on the BC server. Requires a pageContextId from bc_open_page.

After closing, the pageContextId becomes invalid -- any subsequent bc_read_data, bc_write_data, bc_execute_action, or bc_navigate calls using it will fail. It is safe to call this even if prior operations on the page encountered errors. If you opened a drill-down page via bc_navigate (which returns a new pageContextId), close both the drill-down page and the original list page when done.

Do NOT call this on a dialog's pageContextId (from dialogsOpened): answer the dialog with bc_respond_dialog instead, e.g. "cancel" to dismiss it. Closing a modal can accept it, so this tool refuses.

Do NOT call this in the middle of a multi-step workflow -- finish all reads, writes, and actions on the page first. Do NOT call this to "reset" a page; use bc_read_data to refresh data instead.

bc_search_pagesA

Searches BC's Tell Me index for pages, reports, codeunits, and other run-targets matching the query. Each result is { name, objectType, runTarget, departmentPath?, category?, score? } where objectType is "page" / "report" / "codeunit" / etc., runTarget is the BC AL object name (e.g. "Customer List"), and category is the BC department (e.g. "Lists", "Tasks"). Use this when you do not know the page ID for an entity — search by keyword first, then resolve. Do NOT use it when you already know the numeric page ID (call bc_open_page directly), and do NOT use it to read data — it only discovers objects.

Tell Me is PROFILE-SCOPED on the BC server. If the search returns no rows in an env where the BC web client finds matches, set the BC_PROFILE environment variable on bc-mcp's startup config to a profile that indexes the relevant objects (BUSINESS MANAGER, ACCOUNTANT, SALES ORDER PROCESSOR, etc.). The default profile may have an empty Tell Me index.

Note that BC's Tell Me identifies pages by AL name, not by numeric ID. The runTarget is therefore a string like "Customer List" rather than "22". To open the result, the caller currently still needs the numeric page ID: match the runTarget AL name to a known page ID (e.g. "Customer List" = 22), or try bc_open_page with a candidate ID.

Empty-result behavior: response includes a "note" string explaining the likely cause and suggesting BC_PROFILE remediation.

Examples:

  • { "query": "customer" } returns rows like { "name": "Customers", "objectType": "page", "runTarget": "Customer List", "category": "Lists", "score": 9 }.

  • Empty case: { "results": [], "note": "No results. Tell Me is profile-scoped..." }.

bc_navigateA

Navigates to a specific record on an open Business Central List or Document page using its bookmark. Supports two actions: "select" positions the cursor on a row without opening it, and "drill_down" opens the record in its Card/Document page. Requires a pageContextId from bc_open_page and a bookmark from row data returned by bc_open_page or bc_read_data.

Action "select" (default): Positions the cursor on the specified row. Does NOT open the record or return new data -- it only moves the selection. Note: bc_execute_action can target a row directly via its own bookmark/rowIndex parameters, so you usually do not need a separate select before an action like Delete.

Action "drill_down": Opens the record's detail page (e.g., drilling down from Customer List opens Customer Card, drilling down from Sales Orders opens Sales Order). Returns a NEW pageContextId for the opened Card/Document page with its full state. The original List page remains open. Remember to bc_close_page both pages when done.

Section targeting: Use section (e.g., "lines") to navigate within a Document page's subpage repeater. Omit it for the header/default repeater.

Do NOT use this for Card pages -- it only works on pages with repeater rows. Do NOT confuse "select" with "drill_down": select just moves the cursor, drill_down opens a new page. For field-level lookups (enumerating valid values for a related-table field), use bc_lookup, not this tool.

Examples:

  • Select a row: { "pageContextId": "abc", "bookmark": "XXXX", "action": "select" }

  • Drill down to Card: { "pageContextId": "abc", "bookmark": "XXXX", "action": "drill_down" }

  • Drill down from a document line: { "pageContextId": "abc", "bookmark": "XXXX", "action": "drill_down", "section": "lines" }

bc_respond_dialogA

Responds to an open Business Central dialog or confirmation prompt. Dialogs are triggered by bc_execute_action, bc_write_data, or bc_run_report when BC requires user input (e.g., "Do you want to post?", "Delete this record?", validation warnings, or a report request page). When those tools return a dialogsOpened array with requiresDialogResponse: true, or bc_run_report returns a requestPage, you MUST call this tool (response: "ok" for a report request page) to continue the workflow.

The dialogFormId comes from the dialogsOpened array in the triggering tool's response. The response parameter accepts: "ok" (confirm/accept), "cancel" (dismiss/abort), "yes" or "no" (answer a yes/no question), "abort" (only for a dialog that shows an Abort button; on any other dialog BC ignores it and the call fails with INPUT_VALIDATION_ERROR, the dialog still open), "close" (close a modal information page), or "print" / "preview" (press Print or Preview on a report request page that an action opened, e.g. Print... on a posted document: it has no OK button, so "ok" does nothing there). The tool presses the dialog's own button for the response (e.g. "ok" also matches a page's OK that BC models as LookupOk). Choose the response that matches the dialog's intent -- confirmation dialogs typically need "yes", acceptance dialogs need "ok".

After responding, check the changedSections array in the result to see which page sections were affected. For example, posting a Sales Order may change all sections. If the dialog response triggers another dialog (chained confirmations), the response will include a new dialogsOpened array -- respond to each dialog in sequence.

Do NOT call this without a preceding dialog -- there is no dialog to respond to unless a tool returned dialogsOpened (bc_execute_action, bc_write_data, bc_wizard_navigate, bc_respond_dialog, bc_close_page, bc_upload_file), or a requestPage was returned by bc_run_report. Do NOT guess the dialogFormId -- always use the exact value from the dialogsOpened array (or requestPage.formId).

This tool only presses a button; it never sets values. To fill a dialog's input fields (e.g. a request page or a dialog with entry fields), call bc_write_data with the pageContextId from the dialogsOpened entry or requestPage (bc_run_report) -- that is the dialog's own page context -- and then call this tool to press OK. If a write fails, fix it or cancel the dialog; do not press "ok" over a failed field. Pass either that dialog pageContextId or the opening page's; changedSections and openedPages always describe the opening page.

Example: { "pageContextId": "abc", "dialogFormId": "dialog-123", "response": "yes" }

If the action produces a file (Open in Excel, Print, export), its bytes appear in downloads[] (an operationId replay returns the metadata with error code NOT_REPLAYED instead of the bytes); links BC would open externally appear in externalUris[] and are never fetched by the server.

bc_switch_companyA

Switch to a different company within the current Business Central session. All currently open pages will be invalidated and their pageContextIds will become unusable -- you must call bc_open_page to re-open any pages you need in the new company context.

Use bc_list_companies first to see the available company names and verify the target company exists. The companyName must be an exact, case-sensitive match (a wrong-case or unknown name returns COMPANY_NOT_FOUND and leaves the session on its current company). After switching, all subsequent bc_open_page, bc_read_data, bc_write_data, and bc_execute_action calls will operate against the new company's data.

Do NOT switch companies in the middle of a multi-step workflow (e.g., between creating a Sales Order and posting it). Complete all operations in the current company first, then switch.

Example: { "companyName": "CRONUS International Ltd." }

bc_list_companiesA

List all companies available in the current Business Central environment. Returns an array of company names along with the currently active company name. Use this before bc_switch_company to verify the target company exists and to discover available companies.

This tool opens the BC Companies system page internally, reads all entries, and closes it. It does not affect your currently open pages or session state. No parameters are required.

Do NOT use this if you already know the company name -- call bc_switch_company directly. If you need to work with data in a specific company, use bc_switch_company followed by bc_open_page.

bc_run_reportA

Execute a Business Central report by its numeric report ID. If the report has a request page (parameter/filter dialog), the response's requestPage carries its fields plus a requestPage.pageContextId and requestPage.formId. Fill parameters with bc_write_data against that pageContextId, then run the report with bc_respond_dialog { dialogFormId: requestPage.formId, response: "ok" }. The report runs server-side on the BC service tier.

Pass format: "pdf", "excel", or "word" to capture the rendered output as base64-encoded bytes (this path auto-drives the request page, so no bc_write_data/bc_respond_dialog is needed). The tool drives the BC "Send to..." flow (SystemAction 410) internally: opens the format-selection dialog, selects the requested format by SaveValue-ing the matching text label into the SelectionControl, confirms with OK (300), then fetches the file from DynamicFileHandler.axd. When format is set, rendered file bytes are returned in downloads[] (base64 in bytes, or savedPath when BC_DOWNLOAD_DIR is set); an oversized file reports a per-entry error instead of bytes, and an operationId replay reports NOT_REPLAYED (the ledger keeps no bytes).

Format availability depends on the report's installed layouts -- not all reports offer all three formats. If the report does not offer the requested format, an error is returned listing the available option texts. "pdf" is always BC's default and requires no SaveValue; "excel" prefers the "data only" variant; "word" targets any option containing "Word".

Use this tool for reports that perform server-side actions (batch posting via Report 295, inventory adjustments, data processing) or to inspect and fill request page parameters. Common reports: 1306 (Customer Statement), 120 (Aged Accounts Receivable), 6 (Trial Balance), 295 (Batch Post Sales Orders).

Do NOT use this for viewing data -- use bc_open_page and bc_read_data for data retrieval. Do NOT confuse reports with pages -- reports are processing/printing objects, pages are UI views.

Example (open request page): { "reportId": 6 } Example (capture PDF): { "reportId": 6, "format": "pdf" } Example (capture Excel): { "reportId": 6, "format": "excel" } Example (capture Word): { "reportId": 6, "format": "word" }

bc_wizard_navigateA

Drive a Business Central NavigatePage / wizard by semantic step. Use after bc_open_page on a page whose response has isModal: true and pageType: "NavigatePage" (Continia activation wizards, BC setup wizards, request pages with multi-step layouts). The action argument is one of: "next" (advance), "back" (return to previous step), "finish" (complete the wizard), "cancel" (abort).

bc-mcp identifies the navigation buttons by the icon resource BC's own client uses (Actions/PreviousRecord, Actions/NextRecord, Actions/Approve), not by SystemAction or caption -- so localised wizards work without changes. The response surfaces fields visible on the new step, the remaining navigation options (availableNav), and a closed flag set when the wizard finished.

Typical workflow: bc_open_page (returns isModal=true, fields for step 0) -> bc_write_data (fill step 0 inputs) -> bc_wizard_navigate { action: "next" } -> bc_write_data (fill step 1) -> ... -> bc_wizard_navigate { action: "finish" }. The wizard closes itself on finish/cancel; the pageContextId becomes invalid afterwards. A step that opens a separate dialog (e.g. "Set up SMTP Account") returns it in dialogsOpened with its own pageContextId: fill it with bc_write_data against that id and answer it with bc_respond_dialog, not through the wizard's pageContextId.

Do NOT use this for non-wizard pages -- use bc_execute_action instead. Do NOT call "next" past the last step -- use "finish" once availableNav lists it.

Example: { "pageContextId": "abc", "action": "next" }

If the action produces a file (Open in Excel, Print, export), its bytes appear in downloads[] (an operationId replay returns the metadata with error code NOT_REPLAYED instead of the bytes); links BC would open externally appear in externalUris[] and are never fetched by the server.

bc_lookupA

Enumerates candidate values for a related-table (FK) field by invoking BC's built-in Lookup on the field and returning the result rows. Use this when you need to see valid choices for a field before writing it with bc_write_data — for example, listing all Salesperson Codes before filling "Salesperson Code" on a Customer Card, or listing all Gen. Bus. Posting Groups before selecting one.

Use bc_lookup when the field has isLookup=true in the bc_open_page or bc_read_data response. The field must be on an open page (pageContextId from bc_open_page). The operation is non-mutating: it opens the lookup form and always cancels without selecting a value, leaving the source page field unchanged. Provide an optional search string to filter candidates (e.g., search:"AR" to narrow to codes starting with "AR").

Do NOT use bc_lookup for option/enum fields — those already expose their fixed choices in the options array of bc_open_page and bc_read_data responses. Do NOT use for fields where isLookup is false or absent. Do NOT use for fields that carry lookupCustom=true in the bc_open_page or bc_read_data response: isLookup=true together with lookupCustom=true means the field drives a custom AL OnLookup trigger that BC does not expose as an enumerable lookup form — bc_lookup returns a clear error for these. Use the field's own UI/AssistEdit instead.

Workflow: bc_open_page → inspect field isLookup=true → bc_lookup to list candidates → bc_write_data with chosen value.

bc_queryA

Reads records from Business Central in bulk using the Standard API v2.0 (OData/REST on port 7048). Use bc_query for efficient server-side filtered, sorted, and projected reads over many records — for example, fetching all open sales orders, listing customers in a city, or pulling G/L entries for a date range. This is far more efficient than using bc_open_page + bc_read_data for bulk reads because filtering and projection happen on the server before any data is transferred.

When to use bc_query: structured data retrieval over standard BC entities, when you need 2+ records with specific field selection, when you want server-side filter/sort/OData operators ($filter, $select, $top, $orderby, $expand), or when you need to inspect a large dataset without driving the BC UI. Entity names are BC Standard API v2.0 names (camelCase): customers, vendors, items, salesOrders, salesInvoices, purchaseOrders, purchaseInvoices, generalLedgerEntries, accounts, journals, journalLines, companies, employees, dimensions, dimensionValues, currencies, paymentTerms, shipmentMethods, paymentMethods, countriesRegions, unitsOfMeasure, taxGroups, contacts. Pass filter as OData $filter syntax (e.g., "city eq 'London'", "amount gt 1000", "postingDate ge 2024-01-01"). Pass select as comma-separated field names (e.g., "number,displayName,city") to limit response size. top defaults to 100 if omitted — pass explicitly to get more or fewer rows. Queries are company-scoped automatically; pass company to target a specific company (see bc_list_companies). The special "companies" entity is the one exception — it is the top-level environment list (not company-scoped), so the company parameter is ignored for it; query it to discover available companies.

When NOT to use bc_query: do not use for UI-driven flows (navigating pages, clicking buttons, filling forms — use bc_open_page + bc_execute_action for those). Do not use bc_query for posting, writing, or triggering BC business logic — OData reads are read-only; use bc_write_data and bc_execute_action for mutations. Do not use for custom/extension entities not in the Standard API v2.0 — those require the UI WebSocket tools. Auth: on-prem NavUserPassword uses HTTP Basic; BC Online (SaaS) uses device-code — when sign-in is needed the tool returns DEVICE_LOGIN_REQUIRED with a verification URL and code to show the user, and a retry after they sign in runs the query. bc_query does not need a /csh WebSocket session and does not open the SaaS sign-in window.

bc_upload_fileA

Upload a local file into a Business Central file-upload dialog, driving the page's AL Upload/UploadIntoStream code on the uploaded stream. This is a TWO-STEP flow: first invoke the page's upload action (bc_execute_action, e.g. "Import Layout" on Custom Report Layouts / page 9650) so BC opens the file-picker dialog (a FileUploadForm, returned in dialogsOpened), then call bc_upload_file with that page's pageContextId. The file is sent as a multipart POST to BC's DynamicFileUploadHandler over the same authenticated session, and the dialog is closed afterwards — exactly what the web client does — which resumes the suspended AL code on your bytes.

The upload targets the page's open FileUploadForm dialog automatically; optionally pass formId (the dialog formId from bc_execute_action's dialogsOpened) to target it explicitly. The call FAILS with a clear error instead of uploading when no FileUploadForm dialog is open — BC would otherwise silently discard the bytes (HTTP 200 with no effect), e.g. when the action failed and opened an error dialog instead, or when the invoked action is a declarative fileuploadaction (wire type fla, such as Extension Management's "Upload Extension..."), which uses a different protocol this tool does not serve.

Provide the file as either path (an absolute path to a local file) or base64 (inline content, with fileName).

The response reports the uploaded fileName, sizeBytes, the target formId, and any dialogsOpened the AL code raised in reaction to the file (e.g. a validation error for a bad file — respond to those with bc_respond_dialog, or re-read the page with bc_read_data to see the imported rows). This tool is for pushing a file TO BC; to retrieve files FROM BC (reports, "Open in Excel"), use bc_run_report / bc_execute_action, which return downloads[].

Example (local path): { "pageContextId": "session:page:9650:ab12", "path": "C:/tmp/layout.docx" } Example (base64): { "pageContextId": "session:page:9650:ab12", "base64": "UEsDBAo...", "fileName": "layout.docx" }

bc_get_operationA

Look up what happened to an earlier mutating call made with an operationId (bc_write_data, bc_execute_action, bc_respond_dialog, bc_wizard_navigate, bc_close_page, bc_switch_company, bc_run_report, bc_upload_file). Returns the operation ledger entry: status (sent, completed, unknown), tool, timestamps, replaySafe, the stored result, and the target snapshot captured before the call ran.

When the outcome is unknown or replaySafe is false, this tool also re-reads the target in the current session: it re-opens the page by page id and record bookmark, reads the identified fields (the record's key fields and any written fields, plus the lines of a document such as a sales order) or the targeted row, closes the page again, and returns them as readBack plus evidenceHash, a SHA-256 of the canonical JSON of readBack. A key field marked source 'bookmark' has no control on the page and is decoded from the record bookmark: an Option key shows its ordinal (Sales Order "Document Type" = '1', meaning Order), not its caption. Use it after OPERATION_IN_DOUBT, a timeout, or SESSION_LOST to decide whether the change landed before acting again under a new operationId.

This tool changes nothing in BC. A completed, replay-safe operation is returned without a read-back. If the operation ran in another company, readBackError COMPANY_MISMATCH tells you to bc_switch_company first. If a dialog is open in the session, readBackError MODAL_OPEN: answer it with bc_respond_dialog first (opening a page would cancel it). If the record no longer exists (a fully posted document is deleted), readBackError RECORD_NOT_OPENED. If the re-opened page shows a different key value than the recorded one, readBackError KEY_MISMATCH returns readBack WITHOUT an evidenceHash: the page landed on another record, so the read-back proves nothing about the target. Do NOT use it to read arbitrary records -- use bc_open_page and bc_read_data for that.

bc_run_testsA

Run AL test codeunits in Business Central through Microsoft's AL Test Runner (page 130455 "Command Line Test Tool", the same path BcContainerHelper uses) and return one structured row per test method. Runners 130450/130451 run the full before-test hook, so the Permissions Mock, TestPermissions and test personas apply; a dev-endpoint test hub does not, and restrictive tests can pass there as SUPER.

Select tests with exactly one of codeunits (a BC filter: "50490..50494", "50490|50492") or extensionId (app GUID). Narrow to methods with a name filter ("Persona"). The suite (default "MCP") is rebuilt on every call and stays in BC with the last run's results, so it does not touch BcContainerHelper's "DEFAULT" suite unless you name it.

The tool runs one codeunit per round trip until all are done. Each row has codeunitId, codeunitName, method, status (passed/failed/skipped/notRun), durationMs, output (the failure message) and callStack for failures, the same row shape as bc-dev-mcp's bcdev_test_run. outcome is "passed", "failed", "noTests" (the filter matched nothing) or "aborted" (the run stopped early, e.g. a test opened an unhandled dialog or the session dropped; results holds the codeunits that finished).

Use this to run, check or debug AL tests. It wraps the manual route (bc_open_page 130455, bc_write_data on "Suite Name" / "Test Codeunit Range", bc_execute_action "Run Next Test" until "All tests executed."); use those tools directly to inspect the suite's lines afterwards. Do NOT use it on production data with runner 130451 (isolation disabled): test writes are committed there. Returns TEST_RUNNER_NOT_INSTALLED when the Test Runner app is missing. Not idempotent: every call runs the tests again.

Example: { "codeunits": "50490..50494" } Example: { "extensionId": "6c3f0b8e-4f49-4b49-9a49-0049c0de4949", "methods": "Persona" } Example: { "codeunits": "134000", "testRunnerCodeunitId": 130451, "suiteName": "NOISOL" }

bc_data_find_tablesA

Finds Business Central tables by name or by meaning and returns their table IDs and names. This is the first step of the data query workflow: bc_data_find_tables -> bc_data_get_table_schema (exact field names) -> bc_data_get_table_relations (join paths, when joining tables) -> bc_data_query (compile and run an AL query). Use searchMode "keyword" with comma-separated name fragments when you can guess the table name ("customer, ledger entry"), and "semantic" with a plain-language description when you cannot ("posted invoices that are still unpaid"); semantic search ranks the source tables of matching pages and returns a score. Semantic search relies on BC's Copilot metadata search and can come back empty where that is not set up (seen on an on-prem BC 29.1 container); fall back to "keyword" when it returns no tables.

When to use: before writing an AL query for bc_data_query, to discover the table behind a business concept, including tables from extensions and tables that have no API page. When NOT to use: to read records from a known Standard API entity (bc_query is simpler), or to find a page to open in the UI (use bc_search_pages). The data query tools run on BC's own MCP server (BC 29+) and need "Data Query Tools (Preview)" enabled in BC's MCP Server Configuration; otherwise they return BC_MCP_UNAVAILABLE. They need no web-client session and no open page.

bc_data_get_table_schemaA

Returns the fields of a Business Central table as compact pipe-delimited text: a header with the table name, ID and primary key, then one line per field with its exact name and type, option values for Option fields, the formula for FlowFields (e.g. "FlowField: Sum("Cust. Ledger Entry"."Amount")"), and TableRelation targets ("rel: Country/Region"). Field names differ between environments and installed extensions, so call this for EVERY table before writing an AL query for bc_data_query — a wrong name is a compile error (AL0345).

Use fieldClass to list only Normal fields, FlowFields or FlowFilters, and nameContains to keep the answer small on wide tables (e.g. ["No.", "Amount", "Date"]). Get table IDs from bc_data_find_tables. When NOT to use: to read field values (this returns metadata only; run a query with bc_data_query, or read a page with bc_open_page). Runs on BC's own MCP server (BC 29+, "Data Query Tools (Preview)" enabled); needs no web-client session.

bc_data_get_table_relationsA

Lists the TableRelation links of a Business Central table, so you can join tables correctly in an AL query. direction "outgoing" returns the tables this table points to and the fields that link them (e.g. Customer -> Country/Region via "Country/Region Code"); "incoming" returns the tables that point to this one (e.g. which ledger tables reference Customer), capped at 50 source tables with incomingTruncated/incomingTotalCount when cut. Conditional relations carry a "conditions" text (e.g. "Type=CONST(Item)").

Use it before writing a multi-dataitem query for bc_data_query: the via-field gives the DataItemLink. Pass relatedToTableIds to ask only about specific tables, which is also how to get past a truncated incoming list. Follow up with bc_data_get_table_schema for exact field names and types. When NOT to use: for single-table queries, or to read data. Runs on BC's own MCP server (BC 29+, "Data Query Tools (Preview)" enabled); needs no web-client session.

bc_data_queryA

Compiles an AL query object you write and, with returnData: true, runs it against any Business Central table the user can read — including tables with no API page and tables from extensions — and returns a page of rows. BC compiles the query on the fly as a dynamic object (nothing is published) and always runs it ReadOnly with the user's permissions and security filters. Without returnData it only compiles and returns the columns and dataitems, which is a cheap way to validate a query; compile errors come back as DATA_QUERY_ERROR with AL diagnostics (e.g. AL0345 for an unknown field) that you can fix and retry.

Workflow: bc_data_find_tables (table IDs) -> bc_data_get_table_schema (exact field names, for every table) -> bc_data_get_table_relations (join fields) -> bc_data_query. Joins, filters, aggregations (Method = Sum/Count/...), date grouping and OrderBy all work, so prefer letting BC aggregate over fetching raw rows. FlowFields can only be filtered with ColumnFilter, not DataItemTableFilter (AL0672). The bc-data-query skill has syntax and worked examples. Results are paged: top is 1-50 (default 10), skip pages forward, totalCount comes on the first page, hasMore says whether another page exists; rows are arrays in column order.

When to use: ad-hoc reads, totals and joins over data the Standard API does not expose, or when bc_query's entities do not fit. When NOT to use: to change data (read-only; use bc_write_data / bc_execute_action), for simple reads of Standard API entities (bc_query), or to see what a page shows (bc_open_page). Needs BC 29+ with "Data Query Tools (Preview)" enabled in BC's MCP Server Configuration, else BC_MCP_UNAVAILABLE (Microsoft has announced billing for these tools at general availability). No web-client session needed.

bc_api_searchA

Searches the Business Central APIs available to this user and returns action names such as "List_Customers_PAG30009", "List_CustomerLedgerEntries_QRY30302" or "Create_Customer_PAG30009". It covers every installed API page and API query the user can read — Microsoft's standard APIs, ISV and custom extension APIs — not only the Standard API v2.0 entities bc_query knows. Names are <PAG|QRY>; List actions read, Create/Modify/Delete and bound/unbound actions change data.

Workflow: bc_api_search -> bc_api_describe (fields, filters, required parameters) -> bc_api_list to read, or bc_api_invoke to create, modify, delete or run an action. Matching is by name fragments, so search with entity words ("customer", "sales invoice", "ledger"); pass actionTypes ["List"] to find readable data only. Write actions appear only for API objects an admin enabled with those permissions in BC's MCP Server Configuration; reads are available for all installed APIs.

When NOT to use: to read tables that have no API (write an AL query with bc_data_query), or to work through the UI (bc_open_page). Runs on BC's own MCP server (BC 29+, API tools enabled), else BC_MCP_UNAVAILABLE; needs no web-client session.

bc_api_describeA

Returns the full schema of one Business Central API action found with bc_api_search: what it does, and the JSON schema of its parameters. For a List action the schema's "_availableFields" lists every field with its type and option values, and says which fields can be used in filter, orderby and select. For Create the properties are the editable fields; for Modify the required "id" and "If-Match" (the record's current "@odata.etag") plus the editable fields; for Delete the key; for a bound action the key and the action's own parameters.

Call it before bc_api_list (to know the exact field names for filter/select/orderby) and before every bc_api_invoke (to send exactly the parameters the action takes; bc_api_invoke's "parameters" object uses the property names from this schema). When NOT to use: to discover action names (bc_api_search) or to read data (bc_api_list). Runs on BC's own MCP server (BC 29+, API tools enabled); needs no web-client session.

bc_api_listA

Reads records through any Business Central API List action found with bc_api_search, with server-side OData filter, select, orderby, top and skip. Rows come back as JSON objects with the API's field names plus "@odata.etag" for API pages (pass that etag as "If-Match" when modifying the record with bc_api_invoke). totalCount is the number of matching records and hasMore says whether another page exists; top defaults to 10.

Use it for any API page or API query — standard, ISV or custom — including ones bc_query does not know (bc_query covers only the Standard API v2.0 entities). Check field names with bc_api_describe first; a filter on a non-filterable field is rejected. When NOT to use: for tables without an API (bc_data_query runs an AL query over any table), or to change data (bc_api_invoke). Read-only. Runs on BC's own MCP server (BC 29+, API tools enabled); needs no web-client session.

bc_api_invokeA

Runs a Business Central API action that changes data: Create_* (insert a record), Modify_* (update one), Delete_* (delete one), or a bound/unbound action such as posting a document. Get the action name from bc_api_search and its exact parameters from bc_api_describe, then pass them as the "parameters" object. Create and Modify return the record as BC saved it, including its new "@odata.etag"; Delete returns no result.

Modify needs the record's key ("id") and "If-Match" set to its current "@odata.etag" (read it with bc_api_list); a stale etag is refused with API_ACTION_ERROR "Request_EntityChanged" — re-read and retry. BC applies its own validation and business logic; a rejection is API_ACTION_ERROR with BC's message and nothing is changed. Write actions exist only for API objects an admin enabled with those permissions in BC's MCP Server Configuration.

Pass an operationId to make a retry safe: the same id replays the first result instead of running the action again. replaySafe is true for Modify and Delete (a repeat is refused by BC: stale etag, or record gone) and false for Create and bound actions (a repeat would create or post again). When NOT to use: to read (bc_api_list), or for changes that need the UI's dialogs and pages (bc_open_page, bc_write_data, bc_execute_action). Runs on BC's own MCP server (BC 29+); needs no web-client session.

Prompts

Interactive templates invoked by user choice

NameDescription
bc_find_pageFind a Business Central page or report by natural-language name and open it.
bc_read_listOpen a list page, apply filters/sorting, and read a slice of rows.
bc_edit_recordOpen a card page to a record and set field values safely (with lookup + validation handling).
bc_create_documentCreate a new document (sales/purchase order, invoice) with header fields and line items.
bc_post_documentPost a sales/purchase document (or run a document action) and handle the confirm dialog + result.
bc_set_dimensionsRead or set dimensions on a document or card via the Dimensions sub-editor.
bc_reportRun a Business Central report: fill its request page and/or capture the rendered output.
bc_bulk_readBulk-read structured data via BC OData (Standard API v2.0) without opening any UI page.
bc_data_analysisAnswer a data question by writing and running an AL query with the bc_data_* tools (BC 29+), over any table, including ones without an API page.
bc_run_wizardDrive an assisted-setup / NavigatePage wizard step by step to completion.

Resources

Contextual data attached and managed by the client

NameDescription
bc-ui-recovery/SKILL.md
bc-data-query/SKILL.md
bc-api-actions/SKILL.md

TDQS

A4.4/5.0

Scored across 25 tools

Disambiguation4/5

Descriptions are unusually explicit about boundaries, with each tool stating when NOT to use it and pointing at the correct alternative (page reads vs bc_query vs bc_api_list vs bc_data_query, bc_lookup vs bc_navigate, bc_write_data vs bc_execute_action). The remaining soft spot is the read surface: standard entities are reachable via both bc_query and bc_api_list, and bc_data_query can also touch API-exposed tables, so an agent has three plausible entry points distinguished mainly by prose rather than by name.

Naming Consistency4/5

Consistent lowercase snake_case with a uniform bc_ prefix and verb-first names (bc_read_data, bc_open_page, bc_execute_action, bc_search_pages). Sub-namespaces (bc_data_*, bc_api_*) follow internal patterns, but the 'data' word is overloaded — bc_read_data/bc_write_data are page operations while bc_data_* are AL table queries — a minor naming wrinkle despite overall predictability.

Tool Count3/5

25 tools sits at the heavy end of the scale; the scope genuinely spans four distinct access modes (UI WebSocket pages, Standard API OData, BC 29 native API/data-query MCP tools, AL test runner), which explains but does not shrink the surface. An agent must choose among many entry points before doing anything, making the set borderline heavy.

Completeness4/5

Coverage is exceptional across the full workflow lifecycle: discovery (pages, tables, APIs), reads at three levels, writes, actions, dialogs, wizards, file upload/download, reports, company switching, and operation replay/read-back for doubted calls. The notable gap is that bc_search_pages returns AL names but the caller still needs a numeric page ID to open a page, an admitted dead end that forces guesswork.

Maintenance

ActivityActive
ResponsivenessLimited data