A Python SDK and CLI for Microsoft Dynamics 365 Business Central APIs, with agent-friendly endpoint discovery, browser auth, custom API registries, and a built-in dlt source for ETL backup pipelines.
Status: Alpha (0.1.x). Public surface may change before 1.0. Track CHANGELOG.md for breaking changes. Independent project by @igor-ctrl — not affiliated with Microsoft.
from bcli import AsyncBCClient
# Programmatic auth — no config files needed
async with AsyncBCClient(
tenant_id="your-tenant-id",
client_id="your-app-id",
client_secret="your-secret",
environment="Sandbox",
company_id="your-company-id",
) as client:
# Query with fluent OData builder
customers = await client.query("customers").filter("city eq 'Chicago'").top(10).get()
# Write with safety gate
async with client.safe_write("Sandbox", "your-company-id") as sw:
await sw.post("salesInvoices", body={"customerNumber": "10000"}, domain="finance")Or use TOML profiles for the CLI and repeated SDK use:
from bcli import AsyncBCClient
async with AsyncBCClient(profile="production") as client:
vendors = await client.query("vendors").select("displayName", "balance").get()# Install (PyPI distribution name is "bc-cli"; the binary is "bcli")
pip install bc-cli
# or
uv tool install bc-cli
# Configure with browser auth (no client secret)
bcli config init
# Query standard APIs immediately
bcli get customers --top 5
bcli get vendors --filter "displayName eq 'Fabrikam'" --format json
bcli get salesInvoices --select number,totalAmountIncludingTax --top 10
# Import custom APIs from a Postman collection
bcli registry import --from-postman ./my_collection.json
# Query custom endpoints (route auto-resolved)
bcli get myCustomEntities --top 5- Works out of the box — 79 standard BC v2.0 entities (customers, vendors, items, GL entries, ...) with zero configuration beyond auth
- Custom API support — Import your custom API pages from Postman collections, JSON, or live
$metadata - Three-tier endpoint resolution — Custom registry -> standard v2.0 -> fuzzy suggestions
- Multi-company — Assign aliases to companies and query across all entities
- OData query builder —
--filter,--select,--expand,--orderby,--top,--skipon every query - Multiple output formats — table, JSON, CSV, NDJSON for pipeline use
- Secure auth — Browser PKCE by default, OS keychain support for automation secrets, token caching, client credentials + device code fallback
- Write safety — SafeContext gate prevents wrong-environment writes, enforces draft status on financial documents
- Programmatic auth — Pass credentials directly for MCP servers, Airflow DAGs, and containers (no config files required)
- Batch operations — Execute sequences of API calls from YAML files
- ETL pipeline — Built-in dlt source for incremental backup to Parquet / DuckDB / Iceberg / Postgres
- Structured logging — JSON request logs with correlation IDs for observability
Sync BC data incrementally to any dlt-supported destination — useful as a Fivetran backup, a warehouse backfill tool, or a standalone ETL runner.
Standalone (any BC tenant, no bcli config required):
import dlt
from bcli.etl import business_central, EntityDef, fivetran_stamper
source = business_central(
tenant_id="...", client_id="...", client_secret="...",
environment="Production",
entities=[
EntityDef(name="customers"),
EntityDef(name="vendors"),
],
multi_company=True, # iterate all BC companies
stampers=[fivetran_stamper()], # add _fivetran_synced / _fivetran_deleted
)
pipeline = dlt.pipeline(destination="duckdb", dataset_name="bc_raw")
pipeline.run(source)Or use the bcli bridge (reuses your profile and custom-API registry):
# List entities the pipeline will sync
bcli --profile prod etl entities
# Incremental sync (uses systemModifiedAt cursor)
bcli --profile prod etl sync --destination filesystem
# Full refresh
bcli --profile prod etl sync --destination filesystem --full-refresh
# Schedule via cron (every 10 min incremental, nightly full refresh)
*/10 * * * * bcli --profile prod etl sync --destination filesystem
0 0 * * * bcli --profile prod etl sync --destination filesystem --full-refreshRequires Python 3.11+.
Heads-up on the package name. The PyPI name is
bc-cli, notbcli. An unrelated 2018-era package squats on thebcliname (an "EC2 Cluster Creator" with no relation to this project) —pip install bcliwill install that, not this. Alwayspip install bc-cli. Once installed, the import name (import bcli) and the CLI binary (bcli) work as documented.
# CLI + SDK
pip install bc-cli
# SDK + ETL (dlt source for backup pipelines)
pip install "bc-cli[etl]"
# Everything
pip install "bc-cli[etl,mcp,telemetry]"
# Via uv (recommended)
uv tool install bc-cli
# From source
git clone https://github.com/igor-ctrl/bcli.git
cd bcli
pip install -e ".[dev,etl]"| Guide | Description |
|---|---|
| Getting Started | First-time setup, authentication, your first query |
| Business Central Admin Setup | Entra app registration and BC permissions from scratch |
| Configuration | Profiles, environments, config file format |
| Authentication | Browser auth, client credentials, device code fallback |
| Querying Data | GET, OData filters, pagination, output formats |
| Write Operations | POST, PATCH, DELETE |
| Custom APIs | Importing from Postman, JSON, or $metadata |
| Multi-Company | Company aliases, cross-entity queries |
| Batch Operations | YAML batch files |
| SDK Usage | Python SDK for developers and MCP servers |
| MCP Server | Drive bcli from Claude Desktop via the bcli-mcp server (preview) |
| Command Reference | Complete CLI command reference |
| For AI Agents | Quick discovery recipes for Claude Code, Cursor, etc. driving bcli on a user's behalf |
| Contributing | Development setup, architecture, testing |
Licensed under the Apache License, Version 2.0. See the NOTICE file for attribution requirements.