Skip to content

feat(mcp): bcli-mcp preview server for Claude Desktop + workflow YAML safety#2

Merged
igor-ctrl merged 4 commits into
mainfrom
feat/mcp-server
May 4, 2026
Merged

feat(mcp): bcli-mcp preview server for Claude Desktop + workflow YAML safety#2
igor-ctrl merged 4 commits into
mainfrom
feat/mcp-server

Conversation

@igor-ctrl
Copy link
Copy Markdown
Owner

Summary

  • MCP server preview (src/bcli_mcp/): FastMCP-based stdio server exposing 4 read-only tools (query, list_endpoints, describe_endpoint, list_companies) for Claude Desktop and Claude Code agent use. Subprocesses bcli so profile resolution, registry filters, and telemetry are inherited automatically.
  • Field-discovery hint on describe_endpoint: fields_discovered: bool plus opt-in discover_fields=True parameter that runs bcli endpoint fields to populate the local cache. Helps agents pick the right column on first attempt instead of round-tripping BC 400s.
  • JSON output for bcli company list and bcli endpoint list/info — required surface for the MCP runner.
  • fix(workflow): reject YAML 1.1 boolean-key trap at workflow load time. Bare no: / yes: / on: / off: parsed as booleans, silently producing broken payloads for BC fields named no (GL account numbers, line numbers). New bcli.workflow.load_workflow_yaml fails fast with a quote-it-fix message. Shipped purchase-invoice example was emitting {"false": "6260-..."}; now correct.

Commits

  • aa37739 fix(workflow): reject YAML 1.1 boolean-key trap at load time
  • b02997e feat(mcp): fields_discovered hint + opt-in field discovery
  • d95da1e feat(mcp): bcli-mcp preview server for Claude Desktop
  • 38e6287 feat(cli): JSON output for company list + endpoint list/info

Test plan

  • uv run pytest tests/ — 418 passed, 5 skipped, 0 failed
  • uv run ruff check src/ — clean
  • End-to-end: uv run bcli batch run examples/create-purchase-invoice.yaml --set vendor_no=V00011 --dry-run resolves params correctly, emits "no": "6260-000000-000" (was "false": ...)
  • Bool-key error path: workflow with bare no: exits 1 with path + suggested fix in stderr
  • MCP server tests: 25 tests in tests/test_mcp/
  • Workflow tests: 82 tests including 9 new in test_loader.py
  • Manual smoke: register bcli-mcp in Claude Desktop config, run a query against sandbox tenant

Adds --format / -f to:
  - bcli company list  → [{id, name, alias, is_default}]
  - bcli endpoint list → [{name, category, custom, supported_ops,
                           key_field, publisher, group, version,
                           description}]
  - bcli endpoint info → object with the list shape plus entity_name,
                         fields, source_table, page_number

Output contracts are fixed and documented in each command's help text so
external consumers can rely on them. The bcli-mcp server (next commit)
shells out to these commands and depends on the JSON shapes.

The default Rich-table output is unchanged.
Adds src/bcli_mcp/ — a preview MCP (Model Context Protocol) server that
lets Claude Desktop and other MCP-aware clients drive bcli. Subprocess-
only architecture: every tool delegates to 'bcli ... --format json' so
profile resolution, auth, retry, telemetry, and the disable_writes gate
are inherited from the CLI for free.

Day-1 tool surface (4 read-only tools, deliberately small):
  - query             - OData query against an entity, top defaults to
                        50 with a 1000 cap so an agent can't pull a
                        whole table into context.
  - list_endpoints    - Honours profile-level disable_standard_api,
                        allowed_categories, allowed_endpoints filters.
  - describe_endpoint - Fields, key, supported ops, route.
  - list_companies    - [{id, name, alias, is_default}] on the active
                        environment.

Mutating commands (post/patch/delete), file uploads, batch runs, and
admin/setup flows are deliberately not exposed. The CLI fall-back path
trips the existing disable_writes prompt for those.

Trust model: __main__ chdirs to $HOME before instantiating FastMCP so
.bcli.toml auto-discovery from cwd can't pick up a hostile project
config inherited from Claude Desktop's launch directory. Documented in
docs/mcp-server.md.

Packaging: new [mcp] extra (depends on [cli] for the typer/rich deps
the subprocesses need + mcp >= 1.0). New 'bcli-mcp' console script.
src/bcli_mcp added to Hatch wheel packages.

Tests: 32 unit tests covering the runner (subprocess wrapper edge
cases, Rich-markup stripping, profile passthrough, malformed JSON,
timeout) and the four tools (argv shape, top-cap enforcement, single-
record wrap, profile passthrough). All 400 tests + 5 skipped pass.

Out of scope (future): saved-query MCP tools, mutating MCP tools,
plugin entry-point system, HTTP/SSE transport, version bump (lands as
[Unreleased]).
Real-world feedback from a Claude Desktop session: when describe_endpoint
returns 'fields': [], the agent can't tell whether the entity has no
fields or whether the local registry just hasn't probed BC yet. Two
fixes land here:

  1. bcli endpoint info --format json now includes fields_discovered:
     bool. False means the cache is cold; the agent should either probe
     via query(top=1) or call describe_endpoint with discover_fields=True.

  2. bcli-mcp describe_endpoint accepts discover_fields: bool = False.
     When true, the tool runs 'bcli endpoint fields <name>' first as a
     side-effect call (new run_bcli_side_effect helper that doesn't
     parse stdout), populates the registry, then returns the now-
     populated metadata. Discovery failures are swallowed so the agent
     still gets a usable response with fields_discovered=false.

Also documents the BC query-object caveat in docs/mcp-server.md: some
endpoints (e.g. customerSales, vendorPurchases) don't support server-
side $orderby or $filter. The recovery pattern is bounded top + client-
side sort, which is what an agent figures out empirically when an
orderby attempt 400s.

Tests: 9 new (4 for run_bcli_side_effect, 4 for the discover_fields
parameter, 1 already covered the default). All 41 MCP tests + 409
project-wide tests pass; lint clean.
Bare `no:` / `yes:` / `on:` / `off:` as YAML keys parsed as booleans,
silently producing broken payloads for BC fields named `no` (GL account
numbers, line numbers). The shipped purchase-invoice example was
emitting `{"false": "6260-..."}` instead of `{"no": "6260-..."}` on the
GL-account field.

New `bcli.workflow.load_workflow_yaml` fails fast at YAML load time,
naming the path and suggesting the quote-it fix. Wired into `bcli batch
run`. Example fixed. 9 tests cover the bool-key paths and round-trip
the shipped example through the loader.
@igor-ctrl igor-ctrl merged commit 03278df into main May 4, 2026
0 of 3 checks passed
@igor-ctrl igor-ctrl deleted the feat/mcp-server branch May 4, 2026 13:09
@igor-ctrl igor-ctrl mentioned this pull request May 4, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant