Skip to content

Latest commit

 

History

362 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

business-central-mcp

Give AI assistants direct access to Microsoft Dynamics 365 Business Central.
Native WebSocket protocol -- no OData, no APIs, no browser automation.

npm version npm downloads license Install in VSCode Download .dxt for Claude Desktop


Overview

Property Value
Language TypeScript / Node 20+
npm package business-central-mcp
BC versions BC27, BC28 (wire-compatible)
Auth On-prem NavUserPassword. BC Online: ESTS cookie session for /csh (no password in env) + device-code for bc_query.
Tools 12
Tests 901 unit/protocol + 111 integration
License MIT

Install

BC Online (sandbox / production): do not put a password in env. Copy the portal URL from your browser and follow SaaS sandbox setup. The snippets below are for on-prem NavUserPassword.

VSCode

Install in VSCode

Click the badge. VSCode opens and prompts for your BC URL, username, and password (on-prem), then writes the configured entry to your user mcp.json. For BC Online, skip the badge and use the SaaS sandbox env (URL + optional email only).

Manual install

Workspace: create .vscode/mcp.json:

{
  "servers": {
    "business-central": {
      "command": "npx",
      "args": ["-y", "business-central-mcp"],
      "env": {
        "BC_BASE_URL": "http://your-bc-server/BC",
        "BC_USERNAME": "your-user",
        "BC_PASSWORD": "your-password"
      }
    }
  }
}

BC Online — same file, no password:

{
  "servers": {
    "business-central": {
      "command": "npx",
      "args": ["-y", "business-central-mcp"],
      "env": {
        "BC_BASE_URL": "https://businesscentral.dynamics.com/<aad-tenant-id>/DEV",
        "BC_USERNAME": "you@tenant.com"
      }
    }
  }
}

Claude Code

claude mcp add business-central \
  -e BC_BASE_URL=http://your-bc-server/BC \
  -e BC_USERNAME=you \
  -e BC_PASSWORD=secret \
  -- npx -y business-central-mcp

Scope it to the current project with --scope project. See claude mcp --help for scoping options.

BC Online (no password):

claude mcp add business-central \
  -e BC_BASE_URL=https://businesscentral.dynamics.com/<aad-tenant-id>/DEV \
  -e BC_USERNAME=you@tenant.com \
  --scope project \
  -- npx -y business-central-mcp

Claude Desktop

  1. Download the latest .dxt from Releases.
  2. Double-click. Claude Desktop opens Settings → Extensions and prompts for BC URL, username, and password (on-prem). For BC Online, use the manual snippet instead — do not store a SaaS password.
  3. Restart Claude Desktop.
Manual install

Edit claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "business-central": {
      "command": "npx",
      "args": ["-y", "business-central-mcp"],
      "env": {
        "BC_BASE_URL": "http://your-bc-server/BC",
        "BC_USERNAME": "your-user",
        "BC_PASSWORD": "your-password"
      }
    }
  }
}

Restart Claude Desktop.

Configuration

Variable Required Default Description
BC_BASE_URL Yes BC server base URL, e.g. http://your-bc-server/BC, or a SaaS portal URL https://businesscentral.dynamics.com/{aadTenant}/{environment}
BC_USERNAME NavUserPassword On-prem username. On SaaS this is only an email prefill for the local sign-in window.
BC_PASSWORD NavUserPassword On-prem password. Ignored on SaaS (never put a SaaS password in env).
BC_AUTH No auto auto (SaaS URL → SaasWeb, otherwise NavUserPassword), OAuth, SaasWeb, or NavUserPassword
BC_AAD_TENANT_ID OAuth (if not in URL) Entra tenant GUID. Taken from a SaaS BC_BASE_URL when present
BC_ENVIRONMENT No from URL SaaS environment name (DEV, sandbox, production)
BC_CLIENT_ID bc_query on SaaS Multi-tenant public Entra app for device-code sign-in (see bc_query on SaaS). UI tools do not need it
BC_OAUTH_SCOPE No user_impersonation + offline_access Override the Entra scope for bc_query device-code
BC_PROFILE No server default Profile id, e.g. BUSINESS MANAGER. Affects which Role Center loads and which pages Tell Me indexes.
BC_TENANT_ID No default On-prem multi-tenant id. SaaS uses the Entra tenant from the URL.
BC_CLIENT_VERSION No 27.0.0.0 Version reported to BC during session open.
BC_APPLICATION_ID No FIN navigationContext.applicationId sent at session open. SaaS and cronus images expect FIN; some on-prem containers expect NAV (see below).
PORT No 3000 HTTP transport port (stdio transport ignores this).
LOG_LEVEL No info debug / info / warn / error.
LOG_DIR No ./logs Directory for log files.
STATE_DIR No {cwd}/.state Per-repo directory for saas-web-cookies.json and oauth-tokens.json (mode 0600). Relative paths resolve against the MCP process working directory (the project you started the agent in). Sessions in the same repo share the file; different repos never share a login.
BC_INVOKE_TIMEOUT No 30000 Per-invoke timeout in ms. Kills hung sessions.
BC_RECONNECT_MAX_RETRIES No 4 Reconnect attempts after session death.
BC_RECONNECT_BASE_DELAY No 1000 Base delay (ms) for exponential reconnect backoff.

On-prem containers: set BC_APPLICATION_ID=NAV

If sign-in and the WebSocket upgrade both succeed but the session dies at OpenSession with NavCancelCredentialPromptException, the server is rejecting the default applicationId (FIN). On-prem BcContainerHelper containers (the onprem artifact type) generally expect NAV:

BC_APPLICATION_ID=NAV

The failure is misleading because authentication and the /csh upgrade complete first (you get a 101); BC only rejects the applicationId inside the OpenSession RPC body. SaaS and cronus images keep the FIN default. Verified against BC 27.1 onprem (see issue #10).

SaaS sandbox setup

You only need the URL from the browser address bar — the same one you use to open Business Central Online:

https://businesscentral.dynamics.com/<aad-tenant-id>/<environment>

<environment> is usually DEV, sandbox, or production. Do not set BC_PASSWORD. Company policy and this server both treat a SaaS password in env as wrong.

  1. Copy that portal URL into BC_BASE_URL (no extra path, no query string).
  2. Optionally set BC_USERNAME to your work email — that only prefills the sign-in form.
  3. Point the MCP at this project (stdio, Grok .grok/config.toml, Claude --scope project, or a workspace mcp.json). Leave STATE_DIR unset so cookies land in {project}/.state/.
  4. Start the agent on a machine with a display (Linux needs DISPLAY or WAYLAND_DISPLAY). Headless CI cannot complete MFA.
  5. Ask the agent to open a page (bc_open_page, e.g. Customer List = 22). A local window (127.0.0.1) opens. Sign in with Microsoft and complete Authenticator there. Do not paste the password into chat or tool arguments.
  6. Retry the tool. Cookies are saved as {project}/.state/saas-web-cookies.json (mode 0600). Later sessions in the same repo reuse them; another repo needs its own sign-in.

Human shortcut (same working directory as the MCP):

npx business-central-mcp login
# from a source checkout:
npx tsx src/stdio-server.ts login

Grok (project-scoped, no password):

# .grok/config.toml  — not committed if it holds a tenant URL you do not want shared
[mcp_servers.business-central]
command = "npx"
args = ["-y", "business-central-mcp"]

[mcp_servers.business-central.env]
BC_BASE_URL = "https://businesscentral.dynamics.com/<aad-tenant-id>/DEV"
BC_USERNAME = "you@tenant.com"

From a source checkout, point command / args at node + node_modules/tsx/dist/cli.mjs + src/stdio-server.ts instead of npx.

Claude Desktop / VS Code (no BC_PASSWORD):

{
  "mcpServers": {
    "business-central": {
      "command": "npx",
      "args": ["-y", "business-central-mcp"],
      "env": {
        "BC_BASE_URL": "https://businesscentral.dynamics.com/<aad-tenant-id>/DEV",
        "BC_USERNAME": "you@tenant.com"
      }
    }
  }
}

The WebSocket is not on the portal host. After sign-in the server discovers the cluster and uses Origin: https://businesscentral.dynamics.com. You never put a cluster URL in config.

bc_query (OData) on SaaS

bc_query does not use the /csh cookie session. When sign-in is needed the first call returns DEVICE_LOGIN_REQUIRED with a https://microsoft.com/devicelogin URL and user code — complete it in a browser and retry; the retry picks up the pending sign-in and runs the query. The refresh token is stored in STATE_DIR/oauth-tokens.json (mode 0600).

bc_query talks to https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/v2.0 with the Bearer token. If BC_CLIENT_ID is not configured it returns OAUTH_NOT_CONFIGURED (device-code that has not been completed returns DEVICE_LOGIN_REQUIRED, above); it never sends Basic.

Which client id signs in (BC_CLIENT_ID)

BC_CLIENT_ID is required for bc_query on BC Online: a multi-tenant public Entra app with delegated Dynamics 365 Business Central / user_impersonation. The publisher registers it ONE time in their own tenant; customer tenants register nothing — each user consents at first sign-in (user_impersonation is user-consentable), and tenants that disable user consent need a one-time admin-consent click.

Do not borrow a Microsoft first-party client (Azure PowerShell 1950a258-… as New-BcAuthContext does, Azure CLI, …): on tenants with Entra first-party hardening the sign-in fails in the browser with AADSTS65002 ("consent between first party application and first party resource must be configured via preauthorization"), which no tenant admin can consent around. Verified live 2026-08-16 — the same sign-in succeeds on one tenant and fails with 65002 on another. A third-party multi-tenant app is structurally immune (65002 only gates Microsoft-owned client/resource pairs).

Create the app (once, in the publisher tenant):

az ad app create --display-name "business-central-mcp" \
  --is-fallback-public-client true \
  --sign-in-audience AzureADMultipleOrgs \
  --required-resource-accesses '[{"resourceAppId":"996def3d-b36c-4153-8607-a6fd3c01b89f","resourceAccess":[{"id":"bce0976a-cb0b-473b-8800-84eda9f8e447","type":"Scope"}]}]' \
  --query appId -o tsv

(996def3d… is the Dynamics 365 Business Central resource; bce0976a… is its delegated user_impersonation scope.) Put the printed appId in BC_CLIENT_ID.

Known wart: when the browser sign-in fails (65002, blocked consent), Entra keeps the device code authorization_pending, so retries re-serve the same doomed code until it expires (~15 min). Fix the client id / consent, wait out or ignore the old code, and retry for a fresh one.

What can it do?

Tool What it does
bc_open_page Open any page by ID -- lists, cards, documents, role centers. Returns the page as sections[] with header, lines, factboxes, and Role Center cuegroup tiles.
bc_read_data Refresh a single section: filter, paginate, slice, project tab/columns. Returns the same Section shape as bc_open_page.
bc_write_data Write field values; BC validates and echoes confirmed values. Section-aware (lines, factboxes, header).
bc_execute_action Run header / row / wizard actions, OR drill down on Role Center cue tiles via cue input.
bc_respond_dialog Handle confirmation prompts and request pages
bc_navigate Select rows, drill down into records, field lookups
bc_search_pages Tell Me search. Returns { name, objectType, runTarget, departmentPath, category, score } per result.
bc_close_page Close a page and free server resources
bc_switch_company Switch to a different company mid-session
bc_list_companies Discover available companies
bc_run_report Execute reports and fill request page parameters
bc_wizard_navigate Drive NavigatePage / wizard flows (back / next / finish / cancel)

How it works

This server speaks BC's internal WebSocket protocol directly -- the same protocol the browser-based web client uses. It was reverse-engineered from decompiled BC server assemblies. No OData endpoints, no SOAP services, no Selenium.

One WebSocket connection per session. All operations serialized through a promise queue. BC27 and BC28 are wire-compatible.

LLM (Claude / Copilot / etc.)
   |
   v   MCP (stdio or HTTP)
business-central-mcp
   |
   v   WebSocket + JSON-RPC
BC Web Service Tier (BC27 / BC28)
   |
   v   internal calls
BC Server
Page output shape

bc_open_page returns the page as a flat list of sections:

{
  "pageContextId": "session:page:21:abc",
  "pageType": "Card",
  "caption": "Customer Card",
  "isModal": false,
  "sections": [
    { "sectionId": "header",                       "kind": "header",  "fields": [...], "actions": [...] },
    { "sectionId": "factbox:Customer Statistics",  "kind": "factbox", "fields": [...] }
  ]
}

Each section carries its own content shape:

  • Card-style (header on Card pages, factbox, requestPage): fields[] and (for header) actions[]
  • List-style (lines on Documents, header on List pages, repeater subpages): rows[] and totalRowCount
  • Cue tiles (Role Center hosted CardParts): cues[] with each tile's name, value, groupCaption, synopsis, hasAction. Drill down with bc_execute_action { section, cue }.

bc_read_data returns a single Section for the requested sectionId (defaults to "header"). The section ID for a FactBox or subpage comes from the bc_open_page response.

Session resilience
  • Automatic reconnect with exponential backoff after session death
  • Handles BC's ~15s NTLM auth slot hold after crashes
  • Auto-dismisses license popups on fresh databases
  • Invoke timeout kills hung sessions and triggers recovery
  • Auto-recovery from LogicalModalityViolationException mid-session: reconciles the modal stack and retries transparently; falls back to session reset when BC keeps a confirm dialog sticky

Key files

File Purpose
src/stdio-server.ts npm bin entry -- stdio MCP transport
src/server.ts HTTP MCP transport entry
src/mcp/ MCP tool registry, schemas, request handler
src/operations/ One handler per tool (bc_open_page, bc_read_data, etc.)
src/services/ Page, data, action, navigation, search business logic
src/protocol/ WebSocket transport, wire types, captures
src/session/ Session lifecycle, modal stack, reconnect
manifest.json Claude Desktop Extension manifest
scripts/build-dxt.ts Builds .dxt artifact for Claude Desktop
.github/workflows/release.yml Builds + attaches .dxt on v* tag pushes
ROADMAP.md Deferred work (Cursor, init wizard)

Development

git clone https://github.com/SShadowS/business-central-mcp
cd business-central-mcp
npm install
npm run start:stdio-direct   # Run from source
npm test                     # unit + protocol tests
npm run test:integration     # Cronus28 integration tests (requires running BC server)
npm run test:saas            # BC Online smoke (needs a signed-in STATE_DIR cookie file)

Roadmap

Cursor support, an interactive init wizard, and a few protocol gaps. See ROADMAP.md for the full list and priorities.


Author: Torben Leth (sshadows@sshadows.dk) License: MIT (see LICENSE)

About

MCP server for Microsoft Dynamics 365 Business Central via reverse-engineered WebUI protocol. Enables AI assistants like Claude to interact with BC through the native WebSocket interface.

Topics

Resources

Stars

37 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages