Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ When the user has `[audit] enabled = true` in `~/.config/bcli/config.toml`,
every write you trigger appends a JSON line to
`~/.config/bcli/audit/<profile>.jsonl` (or whatever the user configured).
Each entry includes `outcome` (`completed` / `failed` / `dry_run`),
`correlation_id` (BC's `x-ms-correlation-request-id`), `latency_ms`, and the
`correlation_id` (BC's `request-id` header, the CorrelationId BC quotes in errors), `latency_ms`, and the
redacted request body. Useful for:

* Showing the user "here's what just happened" after a multi-step task.
Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- One write operation in the SDK: `client.plan(...)` resolves a mutation once
(`WritePlan`), `client.write(plan)` sends it and returns a `WriteResult` with
the body, HTTP status and BC correlation id. `post` / `patch` / `delete` /
`action`, `batch run` steps, batch rollback, `SafeContext`, MCP record writes
and plain `client.post(...)` all go through it.
- Public `client.resolve(...)` (replaces the private `_resolve_url`),
`client.pages(entity, query, company_id=...)`, `client.auth`, and
`plan` / `write` / `resolve` / `count()` on the sync `BCClient`.
- Batch steps accept `publisher` / `group` / `version`; batch dry-run shows each
write step's resolved URL.

### Changed

- Audit entries are emitted by the SDK. Profile-based SDK clients now audit
when `[audit] enabled = true` (previously CLI only); `batch run` writes and
rollback deletes are now audited; `caller` is `cli` / `mcp` / `sdk`.
- `--dry-run` on `post` / `patch` / `delete` / `action` now fails (exit 1) for
an endpoint the real run cannot route, instead of previewing a null URL.
- MCP `bcli_post` / `bcli_patch` / `bcli_delete` run in-process via the SDK and
return the envelope plus `body`; other subprocess-backed tools no longer
block the server's event loop.
- The dlt source uses bcli's transport and URL validation, and
`bcli_profile` / `load_entities_from_bcli_registry` honour the profile's
`disable_standard_api` and allow-lists. An unknown profile now raises
`ConfigError` instead of returning no entities.
- A batch patch/delete step without `id` reports `missing 'id' field`.

### Fixed

- The BC correlation id was read from `x-ms-correlation-request-id`, an Azure
Resource Manager header BC never sends, so it was always empty — on errors
too. It is now read from BC's `request-id` header, the value BC's own error
messages quote as `CorrelationId` (verified against SBEnvQD).
- Success audit entries, result envelopes and batch ledger rows recorded
`status=200, correlation_id=None` regardless of the response; they now carry
the real status and BC correlation id.
- `get --company all` let a registry route override an explicit `--publisher`
and read only the first page even with `--all`.
- `get --all` and saved queries with `all_pages` dropped `--top`, `--skip` and
`--count`.

## [0.8.3] - 2026-10-30

### Fixed
Expand Down
8 changes: 5 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,11 @@ Each entry is one JSON object with the following keys:
- `dry_run` — the user passed `--dry-run`; no HTTP call fired but the intent
is recorded.

The SDK (`AsyncBCClient`) does NOT auto-emit. Audit is a CLI-layer ergonomic;
programmatic SDK users get unfiltered access by design and can wire their own
logging.
Entries are emitted by the SDK's write operation (`client.write`), so every
write path is covered: CLI verbs, `batch run` and rollback, MCP record writes,
`SafeContext`, and profile-based SDK clients. `caller` says which (`cli`,
`mcp`, `sdk`). A programmatic client (`AsyncBCClient(tenant_id=...)`) has no
config file and so no audit log.

## File Locations

Expand Down
34 changes: 20 additions & 14 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ Desktop, but it also works with the official [MCP Inspector](https://github.com/
and any other client that speaks the spec.

The server generates its tool list **dynamically** by subprocessing
`bcli describe --format json` once on startup, and delegates every call to the
bcli CLI as a subprocess. Profile resolution, auth, retry, telemetry, and the
read-only `disable_writes` gate are inherited from the CLI for free. New CLI
commands light up automatically as MCP tools; deprecated ones disappear.

Read commands return parsed stdout JSON. Mutating commands (`bcli_post`,
`bcli_patch`, `bcli_delete`, `bcli_attach_upload`, `bcli_batch_run`) pass
`--result-out <tmp>` and return the AIP §Phase 2 result envelope content as
the tool result. A `status="failed"` envelope surfaces as an MCP `ToolError`
with the BC correlation id quoted so the agent can cite it.
`bcli describe --format json` once on startup. New CLI commands light up
automatically as MCP tools; deprecated ones disappear.

Record writes (`bcli_post`, `bcli_patch`, `bcli_delete`) run in-process
through the SDK's write operation (`client.plan` + `client.write`): the same
URL resolution, audit (`caller = "mcp"`) and BC correlation id as the CLI.
They return the AIP §Phase 2 result envelope fields plus the response `body`.
A `disable_writes` profile is refused unless the agent passes `yes=true`; any
failure raises an MCP `ToolError` quoting the correlation id.

Read commands, `bcli_attach_upload` and `bcli_batch_run` still delegate to the
bcli CLI as a subprocess (run off the event loop): reads return parsed stdout
JSON; the two mutating ones pass `--result-out <tmp>` and return the envelope,
and a `status="failed"` envelope surfaces as a `ToolError` with the
correlation id quoted.

## Install

Expand Down Expand Up @@ -203,7 +208,8 @@ Two separate MCP processes, two `mcpServers` blocks in
crash the public MCP, and an OSS install never needs to skip optional
private-package imports.

If exposing mutating tools from a private MCP, the package must either
subprocess `bcli post / patch / delete` (so the CLI's `disable_writes` gate
applies) or reimplement that gate locally — the SDK alone does not enforce
it. See `src/bcli_cli/_safety.py` for the canonical helper.
If exposing mutating tools from a private MCP, use the SDK's `client.plan` +
`client.write` (resolution and audit come with it) and apply the
`disable_writes` rule yourself, as `bcli_mcp._server._make_sdk_write_handler`
does — `write()` does not enforce it. The CLI's interactive version is
`src/bcli_cli/_safety.py`.
2 changes: 1 addition & 1 deletion src/bcli/audit/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AuditEntry:
* ``request_body`` — redacted body that was sent; ``None`` for DELETE.
* ``status`` — HTTP status code; ``None`` if the call never
fired (dry-run, pre-call failure).
* ``correlation_id`` — BC ``x-ms-correlation-request-id`` header; useful
* ``correlation_id`` — BC ``request-id`` header (BC's CorrelationId); useful
when grepping BC server-side logs.
* ``latency_ms`` — round-trip latency; ``None`` if no call fired.
* ``cli_version`` — bcli version string.
Expand Down
3 changes: 2 additions & 1 deletion src/bcli/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

from bcli.client._async import AsyncBCClient
from bcli.client._sync import BCClient
from bcli.client._write import WritePlan, WriteResult

__all__ = ["AsyncBCClient", "BCClient"]
__all__ = ["AsyncBCClient", "BCClient", "WritePlan", "WriteResult"]
Loading
Loading