origo-bc-mcp-server
README.md
# origo-bc-mcp-server
Origo Business Central MCP server — connects AI clients (VS Code Copilot, Claude Desktop, etc.) to Business Central via the Cloud Events API.
**Setting this up for the first time? Start at the [installation overview](#installation-overview)** — it takes four parts, two of which are inside Business Central.
## Features
| Area | Tools | Description |
|------|-------|-------------|
| Discovery | `who_am_i`, `bc_list_tenants`, `bc_list_environments`, `bc_list_companies`, `bc_select`, `bc_get_selection` | Auth check and tenant/environment/company selection |
| Table metadata | `list_tables`, `get_table_info`, `get_table_fields`, `get_table_relations`, `get_table_permissions`, `get_page_url` | AL table/field schema introspection |
| Data records | `get_records`, `set_records`, `get_record_ids`, `get_document_lines`, `batch_records` | Read/write any BC table via Cloud Events |
| Search | `search_customers`, `search_vendors`, `search_items`, `search_gl_accounts`, `search_bank_accounts`, `search_employees`, `search_contacts`, `search_resources`, `search_fixed_assets`, `search_projects`, `search_records` | Full-text search across common master data |
| Totals & aging | `get_record_count`, `get_decimal_total`, `compute_customer_aging`, `compute_vendor_aging`, `compute_period_breakdown`, `compute_period_to_date` | Aggregations without pulling raw rows |
| Message types | `list_message_types`, `get_message_type_help`, `call_message_type`, `invoke_message_type` (lite) | Generic access to any BC Cloud Event message type |
| Queue | `queue_get_status`, `queue_retry`, `queue_cancel` | Manage async Cloud Event queue tasks |
| Translations | `list_translations`, `get_field_translations`, `get_field_translation`, `set_field_translation`, `set_translations` | Multi-language field translation management |
| Integration timestamps | `get_integration_timestamp`, `set_integration_timestamp`, `reverse_integration_timestamp` | Track last-sync watermarks for external integrations |
| Memory & config | `list_company_memory`, `get_company_memory`, `set_company_memory`, `list_user_memory`, `get_user_memory`, `set_user_memory`, `get_config`, `set_config` | Persistent notes/config stored in BC's Cloud Event Config Store |
| Incoming documents | `create_incoming_document`, `extract_incoming_document_attachments`, `process_incoming_document` | Upload and process incoming document attachments |
| Crypto | `encrypt_data`, `encode_base64`, `decode_base64` | AES-256-GCM encryption and base64 helpers |
| Business events | `list_bc_business_event_definitions`, `list_bc_business_event_subscriptions`, `create_bc_business_event_subscription`, `delete_bc_business_event_subscription`, `renew_bc_business_event_subscription` | Manage `[ExternalBusinessEvent]` subscriptions |
| **API/OData testing** | `bc_list_api_endpoints`, `bc_get_api_metadata`, `bc_api_request` | Discover, inspect, and test any BC API v2.0 or custom API page endpoint — full CRUD (GET/POST/PATCH/DELETE), `$metadata` parsing (fields, keys, nav properties), and OData query support (`$filter`, `$select`, `$top`, `$orderby`, `$expand`) |
| **Developer Services** | `bc_dev_get_metadata`, `bc_dev_get_symbols`, `bc_dev_publish_app`, `bc_dev_publish_artifact`, `bc_dev_uninstall_app`, `bc_dev_unpublish_app`, `bc_dev_run_tests` | AL publish/cleanup against BC Developer Services + Automation API; Cosmo SSH unit tests |
| **Cosmo Alpaca** | `cosmo_list_containers`, `cosmo_get_container`, `cosmo_create_container`, `cosmo_update_container`, `cosmo_delete_container`, `cosmo_deploy_app`, `cosmo_get_app_info`, `cosmo_ssh_info`, `cosmo_ensure_ssh`, `cosmo_restart_nst`, `cosmo_whoami_config` | Cosmo Alpaca container lifecycle, feed deploy, SSH info and recovery, NST restart (Bearer; independent of BC auth) |
| Skills | `get_cloud_events_api_skill` | Bundled reference docs for the Cloud Events API |
## Installation overview
A working connection has four parts, and all of them have to be right. The most
common failure is finishing Part A and assuming that is the install — Parts C and
D are inside Business Central and are easy to miss.
| Part | Where | What |
|------|-------|------|
| **[A](#part-a--your-machine)** | Your machine | Install the MCP server and configure the connection |
| **[B](#part-b--entra-azure)** | Entra (Azure) | App registration with access to BC |
| **[C](#part-c--origo-cloud-events-core-extension)** | Business Central | Install the Origo Cloud Events Core extension |
| **[D](#part-d--register-the-app-in-bc-and-grant-permissions)** | Business Central | Register the app and grant it permissions |
Part A on its own gives you a server that starts, authenticates, and returns
nothing useful. When you are done, run the [verification sequence](#verify-the-whole-chain) —
each step proves one part.
---
## Part A — Your machine
### 1. Prerequisites
- **Node.js 22+** — [nodejs.org](https://nodejs.org) (LTS recommended)
```bash
node --version
```
### 2. Install from GitHub
```bash
npm install -g github:businesscentralal/origo-bc-mcp
```
npm clones the repository with git, so you need access to it (an SSH key or a
signed-in credential helper) before this command will succeed.
Verify:
```bash
origo-bc-mcp-server --help
```
### 3. Configure the connection
Run the interactive setup wizard:
```bash
origo-bc-mcp-server setup
```
The wizard walks you through:
1. Connection type (SaaS or on-prem)
2. Credentials (client secret, refresh token, or web service key)
3. Secret storage (DPAPI on Windows, Keychain on macOS)
4. Connection validation
5. MCP client configuration (`mcp.json` for VS Code)
6. Desktop shortcut (optional)
To add a single connection without running the whole wizard:
```bash
origo-bc-mcp-server add sandbox1
```
### 4. The settings file
Settings land in `~/.origo-bc-mcp/local.settings.json` (macOS/Linux) or
`%USERPROFILE%\.origo-bc-mcp\local.settings.json` (Windows). A SaaS connection
looks like this:
```json
{
"devConnection": {
"tenantId": "<entra-tenant-guid>",
"clientId": "<app-client-id>",
"authType": "s2s",
"clientSecret": "keychain:origo-bc-mcp-default-secret",
"environment": "Sandbox1",
"companyId": "Vorpun"
}
}
```
That is everything a stdio connection needs. `devConnection` is the only BC
credential — the app authenticates service-to-service (S2S) with a client ID and
a secret.
- **`companyId` must be current.** A stale company name gives
`Company '<name>' not found` on the first call.
- **Store secrets behind a prefix** — `keychain:` (macOS), `dpapi:` (Windows),
`env:` or `aes:`. Never in plain text.
- **Validate the JSON after any manual edit.** The server reads this file inside
a `try`/`catch` and falls back to empty settings when it cannot parse it. No
error mentions the file — you only see `No auth context` later, from a tool
call. A single stray comma is enough. Quick check:
```bash
python3 -m json.tool ~/.origo-bc-mcp/local.settings.json
```
#### About `basicAuth`
The setup wizard usually also writes a `basicAuth` block. It does not apply to
stdio connections and can be left alone:
```json
"basicAuth": { "enabled": true, "username": "dev", "password": "<password>" }
```
`basicAuth` protects the server's HTTP endpoints and the dashboard (`/dashboard`)
when it runs in HTTP mode. Over stdio there is no HTTP traffic and no headers are
sent, so the credentials are never used.
The one visible effect: the username becomes a label that shows up as the
`principal` in `who_am_i`. That is not a BC user and it does not exist in BC. The
real identity is `user.userName` in the `who_am_i` response — the name of the
Entra app as registered in BC.
### 5. Connect your MCP client
#### Claude Code (stdio — recommended)
Create `.mcp.json` in the project folder:
```json
{
"mcpServers": {
"origo-bc-mcp-sandbox1": {
"command": "origo-bc-mcp-server",
"args": ["--stdio"]
}
}
}
```
This is a stdio connection — the server runs as a child process, with no network
service and no port. That is why it does not show up under **Connections** in the
app: it is scoped to that folder.
To use a connection other than `devConnection`, set `MCP_CONNECTION` in an `env`
block. See [Stdio auth for BC tools](#stdio-auth-for-bc-tools).
#### VS Code
The `setup` wizard writes VS Code's `mcp.json` for you. For other clients over
HTTP, see [Configure an MCP client](#configure-an-mcp-client).
---
## Part B — Entra (Azure)
An app registration must exist with:
- **Client ID** and **Tenant ID** — these go into `local.settings.json`
- **Client secret** — stored in Keychain / DPAPI / an environment variable
- **API permission:** Dynamics 365 Business Central → `API.ReadWrite.All` (Application)
- **Admin consent** granted
This is access *to the door* only. What the app may actually do inside BC is
decided in Part D.
---
## Part C — Origo Cloud Events Core extension
The server runs every data call and every action through the custom API that this
extension publishes:
```
/api/origo/{bifrost|cloudEvent}/v1.0/companies(<guid>)/tasks
```
(The server tries the `bifrost` group first and falls back to the legacy
`cloudEvent` group — `cloudevent` on-prem.)
Without the extension almost nothing works — not `who_am_i`, `get_records`,
`list_message_types`, nor any message type.
Install it in BC: **Extension Management → Manage → Extension Marketplace**
(AppSource), or **Upload Extension** for a `.app` file.
> **Silent failure warning.** The standard BC API (`/api/v2.0/`) keeps working
> perfectly without the extension. So `bc_api_request` returns correct data while
> `get_records` returns empty results and no error at all. It looks like an empty
> company, not a broken install. **When those two disagree, suspect the extension.**
---
## Part D — Register the app in BC and grant permissions
This is the part that gets forgotten most often.
### 1. Register the app
Search BC for the **Microsoft Entra Applications** page (formerly **AAD
Applications**).
- Create an entry with the app's Client ID
- Set **State = Enabled**
This is inside Business Central, not the Entra portal. The app does not appear
under **Users** — S2S apps live on their own page.
### 2. Grant permissions
Under **User Permission Sets** on that entry:
**Test environments** — one set is enough:
| Set | Name |
|-----|------|
| `CE FULL ACCESS ORI` | Full Access |
**Production** — least privilege. The base set plus whichever gates you need:
| Set | Name | Opens |
|-----|------|-------|
| `CE API ACCESS ORI` | API Access | Base — always required |
| `CE JOB POST ORI` | Project Posting Gate | Posting to projects |
| `CE WHSE POST ORI` | Warehouse Posting Gate | Picks and warehouse postings |
| `CE ITEM POST ORI` | Item Posting Gate | Item ledger entries |
| `CE G/L POST ORI` | G/L Posting Gate | General ledger postings |
| `CE READ ALL ORI` | Read-Only | Read access only |
The app also needs standard BC permissions (`D365 FULL ACCESS` or equivalent).
`CE READ ALL ORI` is **not** sufficient on its own — not even for pure reading.
Every call writes a record to the Cloud Event Message table, so the base access
has to be there.
### 3. Change Log Write Guard (writes only)
The extension protects writes with an allowlist. A write to a field that is not
on it stops with:
```
Field "1" in table 167 must be included in the change log write guard setup
to be updated via Cloud Events.
```
If the MCP connection needs to change particular fields, add them to the **Change
Log Write Guard** setup in BC. This is a deliberate safety valve — do not work
around it without a reason.
---
## Verify the whole chain
Run these in order. Each step proves one part:
| # | Call | Should return | Points at |
|---|------|---------------|-----------|
| 1 | `who_am_i` | `status: Success` and the app's BC user name | Part C or D missing |
| 2 | `list_message_types` | ~22 namespaces (Help, Data, Sales, Warehouse …) | Part D — permissions |
| 3 | `get_records` on `Customer` | Real records | Part C — empty result, no error |
| 4 | `bc_api_request` on `customers` | The same records | Part A or B |
If step 4 returns data but step 3 does not, the extension (Part C) is missing or
the permission sets are short.
## What the connection can and cannot do
**Can:** read and write any BC table through `get_records` / `set_records`, run
~80 message types (posting sales and purchase documents, approvals, warehouse
documents, and so on), and call the standard API directly.
**Cannot:** run arbitrary UI actions. An action that is a button on a BC page is
only reachable if it has been published as a message type. For example, **Create
Inventory Pick** on a project does not exist as a message type — the only pick
action is `Warehouse.Pick.Create`, from a warehouse shipment. Actions like that
have to be run in the UI.
List what is available with `list_message_types`, and get usage details with
`get_message_type_help`.
## Managing connections
### Add a connection
Add a new connection without running the full setup wizard:
```bash
origo-bc-mcp-server add production
origo-bc-mcp-server add sandbox
origo-bc-mcp-server add # prompts for name
```
This asks for connection details, validates, saves to `local.settings.json`, and registers the MCP entry in VS Code's `mcp.json`.
### List connections
```bash
origo-bc-mcp-server remove # lists available connections without removing anything
```
### Remove a connection
```bash
origo-bc-mcp-server remove production
```
Removes the named connection from `local.settings.json` and its entry from VS Code's `mcp.json`. Prompts for confirmation.
### Create a desktop shortcut
```bash
origo-bc-mcp-server shortcut # shortcut for default server
origo-bc-mcp-server shortcut production # shortcut for a named connection
```
Creates a double-clickable shortcut on your Desktop to start the server:
- **Windows:** `.lnk` file (opens cmd)
- **macOS:** `.command` file (executable shell script)
- **Linux:** `.desktop` file
### Clean all config
```bash
origo-bc-mcp-server clean
```
Removes the entire `local.settings.json`, all `origo-bc-*` entries from VS Code's `mcp.json`, and all desktop shortcuts. Use this to start fresh. Prompts for confirmation.
## Server modes (full vs lite)
By default the server runs in **full mode** — all tool groups from the Features table above are registered (~89 tools). This is best for capable models (GPT-4o, Claude Sonnet, etc.) that handle large tool sets well.
**Lite mode** registers a reduced set (~25 tools) built around `invoke_message_type` as a universal entry point, plus data records, aging, period breakdown, crypto, memory, and the Cloud Events skill doc. Use this for local/smaller LLMs that get confused or slow down with too many tool definitions. The API endpoint testing tools (`bc_list_api_endpoints`, `bc_get_api_metadata`, `bc_api_request`) are **not** included in lite mode.
Enable lite mode with an environment variable:
```bash
MCP_LITE=1 origo-bc-mcp-server # macOS/Linux
$env:MCP_LITE="1"; origo-bc-mcp-server # Windows PowerShell
```
The startup banner confirms which mode is active:
```
origo-bc-mcp listening on :3000 (development, LITE)
LITE MODE: reduced tool set for local LLMs
```
To use lite mode with a named connection or PM2, set `MCP_LITE=1` in that process's environment (e.g. `ecosystem.config.cjs` `env` block for a second PM2 app entry).
## Transports: stdio (recommended) vs HTTP
| Transport | When to use | Network |
|-----------|-------------|---------|
| **stdio** (`--stdio`) | **Grok Bot / Cursor local `command` MCP** on the Architect box (shared by all his agents) | None — process stdin/stdout only |
| **HTTP** (default) | Local dashboard, health checks, Docker on a private host | Binds **`127.0.0.1`** by default (`MCP_HOST`). Docker/PM2 set `MCP_HOST=0.0.0.0` for container publish. Do **not** expose publicly (no public Caddy). |
Remote HTTP `url` MCP from Cursor’s backend cannot reach ORI1058/E4-212 localhost. Stdio runs the server **on the Grok Bot computer (Architect box)**, not on ORI1058.
### Run stdio
```bash
origo-bc-mcp-server --stdio
# or, after build:
npm run start:stdio
```
Same full tool set as HTTP (`bc_dev_*`, `cosmo_*`, and all tools from `buildServer`). Optional: `MCP_LITE=1` for the reduced set.
### Stdio auth for BC tools
HTTP Basic middleware does **not** run on `--stdio`. Auth is installed and re-bound as follows:
1. **Startup** — `MCP_STDIO_AUTH=1` plus a process auth context from `local.settings.json` (`devConnection` or `connections[MCP_CONNECTION]`).
2. **Every `tools/call`** — the MCP request handler is wrapped with `ensureAuthBound` so ALS is re-entered for that invocation (Cursor AddMcpServer can otherwise run handlers outside the startup ALS/`enterWith` tree).
3. **`getAuthContext` fallback** — if ALS and the process fallback are both missing, read `globalThis` (shared across duplicate ESM graphs) then rebuild from `MCP_CONNECTION` / local.settings.
4. **`registerTool` wrap** — every tool callback is wrapped with `withStdioAuth` at registration so Cursor handlers re-bind even when outside the startup ALS tree.
| Source | Role |
|--------|------|
| `MCP_LOCAL_SETTINGS_PATH` (or `~/.origo-bc-mcp/local.settings.json`) | Loads settings |
| `devConnection` | Used when `MCP_CONNECTION` is unset or `default` |
| `connections.<name>` | Used when `MCP_CONNECTION=<name>` (e.g. `bc28-is-grok`) |
| `basicAuth` | Optional; username becomes the stdio principal label. **Credentials are not required** on stdio (no HTTP headers). |
| Cosmo (`cosmo_*`) | Independent — Bearer via `COSMO_BEARER_TOKEN` / `gh`; does **not** need BC auth context (binder is a no-op when BC settings are absent) |
Without a resolvable `devConnection` / named connection, `who_am_i` and `bc_dev_*` fail with `No auth context — request reached a tool without authentication.`
```bash
# default → local.settings.devConnection
origo-bc-mcp-server --stdio
# named connection (e.g. Cosmo Alpaca container wired as bc28-is-grok)
MCP_CONNECTION=bc28-is-grok \
MCP_LOCAL_SETTINGS_PATH=~/.origo-bc-mcp/local.settings.json \
origo-bc-mcp-server --stdio
```
Prefer `env:` / env vars for secrets in `local.settings` (`user`/`key`, client secrets, Cosmo token).
### Grok Bot / Cursor local `command` (mcp.json)
Prefer env for secrets (do not put tokens in tool args). Example:
```json
{
"mcpServers": {
"origo-bc-mcp": {
"command": "origo-bc-mcp-server",
"args": ["--stdio"],
"env": {
"COSMO_BEARER_TOKEN": "<from Cosmo Alpaca session>",
"ADO_PAT": "<optional Azure DevOps PAT for bc_dev_publish_artifact>",
"GITHUB_TOKEN": "<optional for GitHub artifacts>",
"MCP_ENCRYPTION_KEY": "<64 hex chars if local.settings uses aes: secrets>",
"MCP_LOCAL_SETTINGS_PATH": "/path/to/local.settings.json",
"MCP_CONNECTION": "bc28-is-grok"
}
}
}
}
```
Notes:
- `command` is resolved on the **Grok Bot / Architect box** (where the agent runs), not on ORI1058.
- Install the package on that box (`npm install -g github:businesscentralal/origo-bc-mcp` or from the Azure Artifacts feed).
- **BC tools over stdio** need `devConnection` (or `connections[MCP_CONNECTION]`) in `local.settings.json` — see [Stdio auth for BC tools](#stdio-auth-for-bc-tools).
- Connection secrets belong in env or `local.settings.json` with `env:` / `aes:` prefixes — tool `pat` / `token` args are optional overrides only.
- HTTP `url` pointing at ORI1058 localhost is **not** usable from Cursor’s remote MCP path; use stdio instead.
## Start the server
```bash
origo-bc-mcp-server
```
Expected output:
```
origo-bc-mcp listening on 127.0.0.1:3000 (development)
MCP endpoint: http://localhost:3000/mcp
Dashboard: http://localhost:3000/dashboard
Health: http://localhost:3000/healthz
Bind: 127.0.0.1 (local only — set MCP_HOST=0.0.0.0 for Docker)
```
For local HTTP only on loopback (default). Prefer `--stdio` for Grok Bot / Cursor local command.
## Dashboard
The server includes a web dashboard at `/dashboard`:
- **Real-time logs** — SSE stream with filtering, auto-scroll, clear
- **Active sessions** — connected MCP clients
- **Server stats** — uptime, memory, PID, Node version
- **Debug toggle** — enable/disable `MCP_DEBUG` at runtime without restart
- **Setup UI** (`/dashboard/setup`) — manage connections, Basic Auth credentials, validate endpoints
- **Restart / Stop** — PM2-aware controls (in Docker containers)
The dashboard is protected by Basic Auth credentials. On first start with no config, it's open to allow initial setup.
## Custom port
```bash
PORT=3001 origo-bc-mcp-server # macOS/Linux
$env:PORT="3001"; origo-bc-mcp-server # Windows PowerShell
```
## Verify
Check server health:
```bash
curl http://localhost:3000/healthz
```
Validate BC connections:
```bash
origo-bc-mcp-server verify # all connections
origo-bc-mcp-server verify production # specific connection
```
## CLI reference
```
origo-bc-mcp-server [command] [options]
Commands:
setup Guided wizard to configure connections and VS Code mcp.json
add [name] Add a single connection (streamlined)
verify [name] Validate a connection (default: all connections)
remove <name> Remove a specific connection (or list available)
shortcut [name] Create a desktop shortcut to start the server
clean Remove ALL connections, config, and shortcuts
init Create ~/.origo-bc-mcp/local.settings.json from template
Options:
--stdio MCP over stdin/stdout (recommended for Grok Bot / Cursor local)
--config <path> Start with a specific local.settings.json
--debug Verbose logging (stdio → stderr)
-h, --help Show help
```
## Configure an MCP client
**Recommended (stdio):** see [Transports: stdio (recommended) vs HTTP](#transports-stdio-recommended-vs-http) for Grok Bot / Cursor `command` + `args` + `env`.
The `setup` wizard writes VS Code's `mcp.json` automatically (HTTP). For other clients using **local HTTP** (loopback only):
```json
{
"servers": {
"origo-bc-mcp": {
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Basic <base64-encoded username:password>"
}
}
}
}
```
Generate the Basic auth value:
```bash
echo -n 'username:password' | base64 # macOS/Linux
[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("username:password")) # Windows
```
The credentials are the `basicAuth.username` and `basicAuth.password` from your `local.settings.json`.
## Update
```bash
npm install -g github:businesscentralal/origo-bc-mcp
```
Your `local.settings.json` is preserved across updates.
## Uninstall
### Remove the npm package
```bash
npm uninstall -g origo-bc-mcp-server
```
### Remove configuration files
**Windows (PowerShell):**
```powershell
# Remove local settings
Remove-Item "$env:USERPROFILE\.origo-bc-mcp" -Recurse -Force -ErrorAction SilentlyContinue
# Remove MCP entries from VS Code (or use 'origo-bc-mcp-server clean' before uninstalling)
```
**macOS/Linux:**
```bash
rm -rf ~/.origo-bc-mcp
```
### Remove VS Code MCP entries
Either run `origo-bc-mcp-server clean` before uninstalling, or manually edit your VS Code `mcp.json`:
- **Windows:** `%APPDATA%\Code\User\mcp.json`
- **macOS:** `~/Library/Application Support/Code/User/mcp.json`
- **Linux:** `~/.config/Code/User/mcp.json`
Remove any `"origo-bc-*"` entries from the `"servers"` object.
### Remove desktop shortcut (if created)
Delete the "Origo BC MCP" shortcut from your Desktop manually.
### Remove stored secrets
If you used DPAPI or Keychain during setup, the encrypted values are embedded in the config files (already deleted above). Environment variables you set manually (e.g. `BC_DEV_CLIENT_SECRET`) should be removed separately:
```powershell
# Windows — remove a user-level env var
[Environment]::SetEnvironmentVariable('BC_DEV_CLIENT_SECRET', $null, 'User')
```
```bash
# macOS — remove Keychain entry
security delete-generic-password -a mcp-encrypted-conn -s origo-bc-mcp-default-secret
```
## Troubleshooting
Real messages from installs, and what is actually behind them:
| Message | Cause | Fix |
|---------|-------|-----|
| `No HTTP resource was found … /cloudEvent/v1.0/…/tasks` (404) | Cloud Events Core extension not installed | [Part C](#part-c--origo-cloud-events-core-extension) |
| `Sorry, the current permissions prevented the action.` (TableData 10075497 Cloud Event Message IndirectInsert) | App has no CE permission sets | [Part D.2](#2-grant-permissions) |
| `Field "N" in table X must be included in the change log write guard setup` | Field is not on the write allowlist | [Part D.3](#3-change-log-write-guard-writes-only) |
| `Company '<name>' not found` | Stale `companyId` in the settings file | [Part A.4](#4-the-settings-file) |
| `get_records` returns 0 rows but `bc_api_request` returns data | Extension missing — silent failure | [Part C](#part-c--origo-cloud-events-core-extension) |
| `No auth context — request reached a tool without authentication` | `devConnection` missing or unresolvable — **or `local.settings.json` is invalid JSON** | [Part A.4](#4-the-settings-file) |
| `Invalid expression of type: table view` | Wrong filter syntax | Use BC format: `WHERE(Job No.=FILTER(000573))` |
| `Authentication_InvalidCredentials` | Bad or expired credentials | `origo-bc-mcp-server verify` |
| `ECONNREFUSED` when calling BC | Connection settings wrong | `origo-bc-mcp-server verify` |
| `command not found` / `not recognized` | npm global bin not on PATH | Restart the terminal; check `npm bin -g` |
| `Unsupported engine` | Node.js older than 22 | Install Node.js 22+ |
| Port 3000 in use | HTTP mode port conflict | Use a different port (see above) |
| SSL errors against on-prem BC | Self-signed certificate | `NODE_TLS_REJECT_UNAUTHORIZED=0 origo-bc-mcp-server` (dev only) |
## Custom config path
```bash
origo-bc-mcp-server --config /path/to/local.settings.json
```
Or set `MCP_LOCAL_SETTINGS_PATH` environment variable.
## Develop
```bash
cp .env.example .env # fill in BC_CLIENT_ID/SECRET, MCP_ENCRYPTION_KEY, ...
npm install
npm run dev # tsx watch
# or
npm run build && npm start
```
Smoke check:
```bash
curl localhost:3000/healthz
curl localhost:3000/.well-known/oauth-protected-resource
```
### Basic auth
Basic auth secures MCP endpoints and the dashboard. It works in all environments
(local dev, Docker, production). Configure it in one of three ways:
1. **Dashboard Setup UI** — open `/dashboard/setup`, fill in credentials (recommended for Docker)
2. **Environment variables** — set `MCP_ADMIN_USER` + `MCP_ADMIN_PASSWORD` at startup
3. **Config file** — set `basicAuth` in `local.settings.json`:
```bash
cp config/local.settings.example.json config/local.settings.json
# edit: basicAuth.username/password + devConnection
npm run dev
```
Then call the server with Basic credentials:
```bash
curl -u admin:yourpass -X POST localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```
The same credentials protect the web dashboard at `/dashboard`.
The Basic-auth connection is locked to its configured tenant (it cannot cross
tenants), exactly like `x-origo-token`.
`devConnection` supports two shapes:
- **On-prem** (`onPrem: true` + `baseUrl`, `onPremTenant`, `user`, `key`,
`companyId`, `companyName`) — Basic auth against an on-prem BC REST base URL.
Mirrors the legacy `BC_ONPREM_*` mode. `bc_list_companies` returns the
configured company; data calls use `Basic base64(user:key)` against
`{baseUrl}/api/origo/cloudevent/v1.0/...?tenant=...`.
- **SaaS** (`tenantId`, `clientId`, `authType`, `clientSecret` or `refreshToken`,
`environment`, `companyId`) — Entra. `authType: "s2s"` uses the client secret
(the normal case); `authType: "user"` uses a refresh token. See
[Part A.4](#4-the-settings-file) for a complete example.
> On-prem **data** calls (message types) are wired during tool migration; the
> connection, auth header (`onPremAuthHeader`) and company listing are in place.
### Developer Services (`bc_dev_*`)
Tools for AL publish/cleanup against BC **Developer Services** and the **Automation API**.
These talk to a container's `{id}dev` / Automation endpoints — not the Cosmo Alpaca control plane.
| Tool | Purpose |
|------|---------|
| `bc_dev_get_metadata` | `GET {dev}/dev/metadata` — package metadata from Developer Services |
| `bc_dev_get_symbols` | `GET {dev}/dev/packages?…` — writes large binaries to disk (path + size; no base64 dump) |
| `bc_dev_publish_app` | Multipart `POST {dev}/dev/apps?tenant&SchemaUpdateMode=…` (local `appPath` or `appBase64`) |
| `bc_dev_publish_artifact` | Download `.app` / zip from HTTPS, Azure DevOps build artifact, or GitHub Actions artifact/release; publish in dependency order |
| `bc_dev_uninstall_app` | Automation API `Microsoft.NAV.uninstall` (**not** `DELETE /dev/apps`) |
| `bc_dev_unpublish_app` | Automation API `Microsoft.NAV.unpublish` (BC 25.4+; uninstall first) |
| `bc_dev_run_tests` | Run AL unit tests via **Cosmo SSH** (`scp` + `pwsh`/`powershell -File`). Cosmo SSH is **in-container** — uses Client Services / PsTestFunctions (not host BcContainerHelper); structured passed/failed/skipped + failure messages |
**`developerBaseUrl`:** set optional `developerBaseUrl` on `devConnection`, or derive it from on-prem `baseUrl` by replacing a trailing `rest` with `dev` (Alpaca: `…/f0a4d51d4d47rest` → `…/f0a4d51d4d47dev`). After `cosmo_get_container`, wire the derived `restBaseUrl` / `developerBaseUrl` into `devConnection` (company typically CRONUS IS). Verified publish path on Cosmo Alpaca: multipart `/dev/apps` → HTTP 200.
**Uninstall / unpublish:** prefer Automation API (`bc_dev_uninstall_app` / `bc_dev_unpublish_app`). If those fail or the container only allows SSH ops, use `cosmo_ssh_info` then `Uninstall-NavApp` / `Unpublish-NavApp` over SSH.
**`bc_dev_run_tests`:** prefers Cosmo SSH (`GET /Container/Ssh/{id}`). SSH is **usable when `ipAddress` and `privateKey` are present** — do **not** require `available===true` (`available=false` is expected while Starting after Stop→Start). Connects as `sshuser` with `privateKey` (never logged). **SSH recovery:** the Cosmo SSH endpoint can drop out while the container keeps running (the container record has no ssh flag). Before a run `bc_dev_run_tests` (and `bc_dev_build_runtime_package`) therefore enable SSH and wait for it (`ensureSsh`, default on; `sshWaitSeconds`, default `BC_DEV_SSH_ENSURE_WAIT_S` or 300; `allowRestart=true` also permits Stop → Start). The result's `sshEnsure` lists what was done. Remote invoke is **`scp` of local `run-tests.ps1` (+ vendored `PsTestFunctions.ps1` / `ClientContext.ps1`) → `C:\Windows\Temp\…`, then `pwsh -NoProfile -File <remote>`** (fallback `powershell.exe -File`); best-effort remote delete afterward. **Do not** pipe the script on stdin to `pwsh -Command -` (Cosmo Windows OpenSSH aborts after the first `Write-Host`).
**Cosmo SSH is inside the BC container** (not a Docker host). Host-side `Invoke-NavContainerTests` / `Run-TestsInBcContainer` / `Run-AlTests` usually do **not** exist there. The remote script therefore:
1. Dot-sources `C:\Run\Prompt.ps1` when present.
2. If host helpers are missing → **in-container Client Services** path (same approach `Run-TestsInBcContainer` uses *inside* the container): locate Service-folder Newtonsoft + `Microsoft.Dynamics.Framework.UI.Client.dll`, load PsTestFunctions (scp upload or `Install-Module BcContainerHelper`), `New-ClientContext` to local NST `/cs?tenant=…`, `Run-Tests` (tries pages 130455 → 130202 → 130203 → 130409).
3. **Option B (BC 27.5+/28):** if Client Services pages fail (page 130455 removed), best-effort `Invoke-NAVCodeunit` **130201** (CLI Test Runner / TestRunner-Internal) — documented clearly; JUnit may be absent.
4. Soft diagnostics via `Get-NAVAppInfo` if runners fail (toolkit is often already present on Cosmo; do not treat publish as a hard gate).
Brief SSH connect retries while Starting. When ip/key are missing, returns a **blocked** error (no silent fallback) with Stop→Start recreate + `cosmo_create_container`/`sshEnabled=true` hints. Cosmo OpenAPI has **no** `/Container/Exec/{id}/…` test-runner endpoint. `mode=helper` is local-docker only (`containerName`). Reuses stdio/`devConnection` NavUserPassword credentials. Returns structured counts + failure messages (truncated previews; never logs `privateKey` / Nav passwords beyond truncated previews).
**Follow-up (out of scope here):** `alc` compile orchestration.
### Cosmo Alpaca (`cosmo_*`)
Same MCP server as `bc_dev_*`. Cosmo tools call the **Alpaca API** with a Bearer token (container lifecycle, feed deploy, SSH info, NST restart). App publish of arbitrary `.app` / CI artifacts stays on `bc_dev_*`.
#### Config (`origo-bc-mcp`)
Auth and backend resolve in order: tool arg → env → `~/.origo-bc-mcp/local.settings.json` → built-in default. **Prefer env for secrets; never commit tokens.**
| Setting | Env / file | Notes |
|---------|------------|--------|
| Bearer | `COSMO_BEARER_TOKEN` or `cosmo.bearerToken` | Required for `cosmo_*` calls |
| API base | `COSMO_BACKEND_URL` or `cosmo.backendUrl` | Must be Alpaca **API** base, not the bare public host |
**Default `backendUrl` (after this fix):**
`https://cosmo-alpaca-enterprise.westeurope.cloudapp.azure.com/api/alpaca/release`
That matches Cosmo Alpaca VS Code **1.27** OpenAPI `basePath` `{host}/api/alpaca/release`. A bare host (`https://cosmo-alpaca-enterprise.westeurope.cloudapp.azure.com`) yields **nginx 404** on `POST /Container/Container/filter`. Paths are joined as `${backendUrl}/Container/...` — do **not** append `/api/alpaca/release` again if the caller already passes the full API base.
Example `cosmo` block in `~/.origo-bc-mcp/local.settings.json` (prefer env for the token):
```json
{
"cosmo": {
"backendUrl": "https://cosmo-alpaca-enterprise.westeurope.cloudapp.azure.com/api/alpaca/release",
"bearerToken": "env:COSMO_BEARER_TOKEN"
}
}
```
Or only:
```bash
export COSMO_BEARER_TOKEN='…' # preferred over committing bearerToken
# optional override:
# export COSMO_BACKEND_URL='https://cosmo-alpaca-enterprise.westeurope.cloudapp.azure.com/api/alpaca/release'
```
**How to get a Bearer (VS Code 1.27):** `cosmo-alpaca.debugMode` is **not** in the Settings UI. Add `"cosmo-alpaca.debugMode": true` to **User** `settings.json`, then Command Palette → **Get GitHub API token** or **Get Azure DevOps API token** (token copies to clipboard). Set `COSMO_BEARER_TOKEN` from that value.
**Verify without echoing the secret:** `cosmo_whoami_config` returns resolved `backendUrl`, whether a bearer is configured (+ length), and `defaultBackendUrl` / `defaultPublicHost` — never the token value.
#### Container tools (`cosmo_*`)
| Tool | Cosmo API | What it is for |
|------|-----------|----------------|
| `cosmo_list_containers` | `POST /Container/Container/filter` | List/filter containers for the tenant |
| `cosmo_get_container` | `GET /Container/Container/{id}` | Status + derived REST/DEV URLs for `devConnection` |
| `cosmo_create_container` | `POST /Container/Container[/gitHub|/azureDevOps|/standalone]` | Create GitHub, Azure DevOps, or standalone BC container |
| `cosmo_update_container` | `PATCH /Container/Container/{id}` | Start | Stop, `sshEnabled`, display fields, … |
| `cosmo_delete_container` | `DELETE /Container/Container/{id}` | Tear down ephemeral containers |
| `cosmo_deploy_app` | `POST /Container/Exec/{id}/deployApp` | Install from **NuGet | Azure DevOps feeds only** |
| `cosmo_get_app_info` | `GET /Container/Exec/{id}/appinfo` | Installed app info blob from the container |
| `cosmo_ssh_info` | `GET /Container/Ssh/{id}` | SSH endpoint/credentials for NavApp fallback (read-only) |
| `cosmo_ensure_ssh` | `GET /Container/Ssh/{id}` + `PATCH /Container/Container/{id}` | Makes SSH usable: enables it (or starts a stopped container with `sshEnabled=true`), polls until ip + key + TCP are there; `allowRestart=true` also permits Stop → Start. Returns the steps taken, never the key |
| `cosmo_restart_nst` | `POST /Container/Exec/{id}/restartServerInstance` | Restart NST after stubborn publish/uninstall issues |
| `cosmo_whoami_config` | (local) | Confirm backend + token configured (no secret echo) |
Public container host (for `{id}rest` / `{id}dev`) remains
`https://cosmo-alpaca-enterprise.westeurope.cloudapp.azure.com` — that is **not** the Alpaca API `backendUrl`.
#### Cosmo vs `bc_dev_*` boundary
| Concern | Use |
|---------|-----|
| Container CRUD, Start/Stop, SSH info, NST restart | `cosmo_*` |
| Install app from NuGet / Azure DevOps **feed** | `cosmo_deploy_app` |
| Publish local `.app`, GitHub Actions / ADO / HTTPS artifact | `bc_dev_publish_app` / `bc_dev_publish_artifact` |
| Uninstall / unpublish | `bc_dev_uninstall_app` / `bc_dev_unpublish_app` first; SSH `Uninstall-NavApp` / `Unpublish-NavApp` via `cosmo_ssh_info` if needed |
| AL unit tests | `bc_dev_run_tests` (SSH when ip+key present via scp/`-File`; **in-container Client Services / PsTestFunctions**, not host BcContainerHelper; blocked with recreate hint otherwise). Cosmo has no Exec test-runner API. |
#### Ephemeral Cosmo loop (policy)
1. `cosmo_create_container` (CreateBcContainer / CreateGitHubBcContainer / Azure DevOps / standalone)
2. `cosmo_get_container` → wire derived `…/{id}rest` + `…/{id}dev` into `devConnection` (CRONUS IS)
3. `bc_dev_publish_*` / tests / `bc_dev_uninstall_app` (Automation API)
4. `cosmo_delete_container`
Do **not** treat standing personal `bc28-is` or machine **ORI1058** as the long-term test host (avoid polluting personal containers). `bc28-is-grok` is optional interim only. Cosmo lifecycle tools live in **this same MCP server** alongside `bc_dev_*`.
## Status & continuation
Scaffold + dual auth + tenant access guard + discovery tools are in place and
compile/run. Next: migrate the ~40+ BC tools from the legacy server (`api/mcp/tools/*`)
into `src/tools/`, then deploy to dev via Azure DevOps.
Further notes live in the `docs/` folder of the source repository on Azure DevOps
(`BC-PTE-CloudEvents` → **Cloud Events MCP**). They are not part of this published
package:
- **`docs/PROJECT-STATUS.md`** — full state, decisions, tool-migration inventory,
open questions, resume checklist.
- **`docs/RESUME-PROMPT.md`** — ready-to-paste prompt to continue the work later.
- **`docs/local-dev.md`** — how to start the server locally (Basic auth, on-prem/SaaS).
- **`docs/devops-setup.md`** — cross-tenant deploy setup.
## Local install
### Run with Docker
The included `Dockerfile` builds a production image with PM2 for automatic restarts. It sets `MCP_HOST=0.0.0.0` so published ports work; the image is still intended for **private** hosts only (not public Caddy). Configuration is stored in a `/data` volume inside the container and managed through the web dashboard.
#### Step 1: Build the image
```bash
docker build -t origo-bc-mcp https://github.com/businesscentralal/origo-bc-mcp.git
```
#### Step 2: Run the container
Mount a local folder for persistent config storage:
```powershell
docker run -d --name origo-bc-mcp --restart unless-stopped -p 3000:3000 -v "E:\Docker Storage\origo-bc-mcp:/data" -e MCP_ENCRYPTION_KEY=<64-hex-chars> -e MCP_ADMIN_USER=admin -e MCP_ADMIN_PASSWORD=<your-password> -e OLLAMA_PROXY_TARGET=http://<ollama-host>:11434 origo-bc-mcp
```
```bash
docker run -d --name origo-bc-mcp --restart unless-stopped -p 3000:3000 -v /path/to/origo-bc-mcp-data:/data -e MCP_ENCRYPTION_KEY=<64-hex-chars> -e MCP_ADMIN_USER=admin -e MCP_ADMIN_PASSWORD=<your-password> -e OLLAMA_PROXY_TARGET=http://<ollama-host>:11434 origo-bc-mcp
```
> **`MCP_ENCRYPTION_KEY`** encrypts connection secrets (passwords, client secrets) at rest in the volume. Generate one with: `openssl rand -hex 32`
>
> **`MCP_ADMIN_USER` / `MCP_ADMIN_PASSWORD`** secure the dashboard on first boot. Without these, the dashboard is open until you configure Basic Auth in the setup UI.
#### Step 3: Configure via the dashboard
Open **http://localhost:3000/dashboard/setup** in your browser.
On first launch (no config exists), the dashboard is open. Add your first connection and enable Basic Auth — subsequent visits will require login.
The setup page lets you:
- Add SaaS (Entra) or On-Premises BC connections
- Validate connections (test button confirms access and lists companies)
- Configure Basic Auth credentials (used for both MCP access and dashboard login)
- Remove connections
#### Step 4: Connect your MCP client
Point your MCP client (VS Code Copilot, Claude Desktop, Open WebUI, etc.) at:
```
http://localhost:3000/mcp
```
With Basic Auth header using the credentials you configured in the dashboard.
#### Dashboard login
The dashboard is protected by the same Basic Auth credentials configured in Setup. If you haven't configured Basic Auth yet, the dashboard is open (to allow first-time setup).
#### Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_DATA_DIR` | `/data` | Directory for `local.settings.json` (mounted volume) |
| `MCP_ADMIN_USER` | — | Bootstrap admin username (sets Basic Auth on first start if no config exists) |
| `MCP_ADMIN_PASSWORD` | — | Bootstrap admin password (pair with `MCP_ADMIN_USER`) |
| `MCP_ENCRYPTION_KEY` | — | 64 hex characters for AES-256-GCM encryption of secrets at rest |
| `MCP_PUBLIC_URL` | `http://localhost:3000` | Public URL for the server |
| `PORT` | `3000` | Listen port |
| `MCP_DEBUG` | — | Set to `1` to enable debug logging (also toggleable from dashboard) |
| `OLLAMA_PROXY_TARGET` | `http://192.168.16.241:11434` | Ollama server URL for the `/ollama` proxy endpoint |
#### Generating `MCP_ENCRYPTION_KEY`
The key must be exactly 64 hex characters (32 bytes). Generate one with any of these:
```bash
# OpenSSL (Linux/macOS/Git Bash)
openssl rand -hex 32
# Node.js (any platform)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# PowerShell (Windows)
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
```
Store the key securely — if you lose it, secrets encrypted with it cannot be recovered.
#### Docker Compose example
```yaml
services:
mcp:
build: https://github.com/businesscentralal/origo-bc-mcp.git
ports:
- "3000:3000"
volumes:
- ./mcp-data:/data
environment:
- MCP_ENCRYPTION_KEY=${MCP_ENCRYPTION_KEY}
- MCP_ADMIN_USER=${MCP_ADMIN_USER:-admin}
- MCP_ADMIN_PASSWORD=${MCP_ADMIN_PASSWORD}
restart: unless-stopped
```
#### Health check
```bash
curl http://localhost:3000/healthz
```
### Install from tarball
The server is also published to the Azure Artifacts feed `BC-PTE-CloudEvents` for
local dev/test on Windows and macOS. Installing from GitHub
([Part A.2](#2-install-from-github)) is the simpler route and is what these
instructions assume.
If you install from the feed or from a downloaded tarball instead, the setup
guides live in the `docs/` folder of the source repository on Azure DevOps
(`docs/setup-from-tarball.md`, `docs/setup-macos.md`, `docs/setup-windows.md`).
Everything from [Part B](#part-b--entra-azure) onwards is identical either way.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues