Skip to content

SWI-10226 feat: add Express Registration API + zero-to-one auth flow#6

Open
kshahbw wants to merge 58 commits intomainfrom
feat/express-registration
Open

SWI-10226 feat: add Express Registration API + zero-to-one auth flow#6
kshahbw wants to merge 58 commits intomainfrom
feat/express-registration

Conversation

@kshahbw
Copy link
Copy Markdown

@kshahbw kshahbw commented Mar 27, 2026

Summary

  • Adds Express Registration API (createRegistration, sendVerificationCode, verifyRegistrationCode) to the MCP server
  • Supports no-auth APIs via requires_auth flag on _create_server — Express requires no authentication
  • Makes startup credentials optional — server starts without BW_USERNAME/BW_PASSWORD, Express tools are available immediately
  • Adds setCredentials tool so agents can inject credentials mid-session after Express registration, triggering loading of authenticated API servers
  • Renames Express verifyCodeverifyRegistrationCode to avoid operationId collision with MFA's verifyCode

Changes

  • src/servers.py: Added requires_auth param, Express in api_server_info, explicit credential check
  • src/config.py: Credentials now optional (warns instead of raising)
  • src/app.py: Reload callback for post-registration credential injection
  • src/tools/credentials.py: setCredentials tool with idempotency guard
  • test/fixtures/express.yml: Express OpenAPI spec fixture (3 endpoints)
  • test/test_express.py: 5 Express-specific tests
  • test/test_config.py: Optional credentials tests
  • test/test_credentials.py: setCredentials tool tests
  • test/test_servers.py: Updated tool count (50), added express mock
  • README.md: Express Registration docs + tool filtering example

Test plan

  • All 21 tests pass (12 existing + 9 new, no regressions)
  • Express server creates 3 tools with correct operation IDs
  • Express server works without auth credentials (no Authorization header)
  • Tool parameter schemas include required fields
  • Config loads without credentials (warns, doesn't crash)
  • setCredentials updates config and triggers reload callback
  • Verify Express spec URL resolves after API branch merge (blocked until launch)

🤖 Generated with Claude Code

kshahbw and others added 8 commits March 27, 2026 00:52
Adds requires_auth parameter to _create_server, registers Express Registration API (no-auth) in api_server_info, and adds Express-specific tests. Total tool count updated to 49 (Express adds 2 net new tools; verifyCode deduplicates with MFA).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… collision

The Express and MFA APIs both had operationId: verifyCode, causing FastMCP
to silently drop one during import. Renamed to verifyRegistrationCode so
all 50 tools are accessible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ntials tool

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fidelity

- Add idempotency guard to _reload_authenticated_servers (prevents tool
  duplication on repeated setCredentials calls)
- Fix mutable default config={} in _create_server and create_bandwidth_mcp
- Fix Express tests to use requires_auth=False (matches production config)
- Move warnings import to top of config.py

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kshahbw kshahbw requested review from a team as code owners March 27, 2026 05:18
@bwappsec
Copy link
Copy Markdown

bwappsec commented Mar 27, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@kshahbw kshahbw changed the title feat: add Express Registration API + zero-to-one auth flow SWI-10226 feat: add Express Registration API + zero-to-one auth flow Mar 27, 2026
kshahbw and others added 18 commits March 30, 2026 11:12
Adds AGENTS.md (structured reference for AI agents using the bw CLI)
and exposes it as resource://cli_agent_reference. Covers command
semantics, prerequisite graph, workflows, error patterns, and
limitations so agents can plan multi-step operations without
trial and error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds AGENTS.md as a comprehensive reference for AI agents using the MCP server:
tool groups, required env vars, dependency order, common workflows, error patterns,
and limitations. Also registers it as a FunctionResource at resource://mcp_agent_reference
so agents can read it programmatically mid-session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s, voice

Covers instructions-based agent routing (FastMCP instructions field),
HTTP transport for hosted deployment, callback server for inbound events,
programmable voice via BXML with barge-in and redirect-chain patterns,
tool profiles, and local spec caching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop conversation summarization (LLM manages its own context),
single-tenant only, callback server over relay, in-memory event store,
light SSML by default. Two open questions remain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per-session read cursors on the event store so multiple MCP sessions
(two IDE windows, etc.) don't race on callback events. First-write-wins
on voice responses prevents two agents from responding to the same call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers: dynamic instructions, tool profiles, spec caching, event store,
callback routes, callback tools, BXML generation, voice tools, transport
config, docs updates, and integration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds src/instructions.py with build_instructions() that generates
context-aware MCP instructions based on loaded tools and config presence.
Also adds pythonpath config to pyproject.toml so src imports resolve
consistently across all test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces resolve_profile() with named presets (messaging, voice, onboarding, lookup, full) and wires --profile CLI flag + BW_MCP_PROFILE env var into get_enabled_tools() as a fallback when no explicit tool list is provided.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Config resource was added after the test was written. Fixes 5 pre-existing failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Writes cleaned specs to ~/.bw-mcp/spec-cache after a successful fetch and
falls back to the cached version when the network is unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Calls build_instructions after setup() and _reload_authenticated_servers()
so mcp.instructions reflects the actual loaded tool set at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ring-buffer backed EventStore with TTL, per-session read cursors for multi-session
safety, and first-write-wins BXML locking on CallState objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements BXML generation from verb descriptors and a callback-response
flow for active voice calls, with full test coverage across all supported
verb types and edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… into app

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
kshahbw and others added 30 commits April 2, 2026 10:17
…ader

Express Registration spec isn't published to dev.bandwidth.com yet.
Bundle it in src/specs/ and load directly from disk. fetch_openapi_spec
now handles local paths before attempting HTTP fetch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FastMCP.mount() expects another FastMCP server, not a raw Starlette app.
Refactored to use @mcp.custom_route() decorator which registers routes
directly on the MCP server's HTTP transport. Callback routes are now
registered during setup() alongside tools, always available in HTTP mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent can authenticate with just username/password and discover
the account ID from the API afterward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setCredentials now takes client_id and client_secret, exchanges them
for a Bearer token via POST /api/v1/oauth2/token, and extracts account
IDs from JWT claims — same flow as the Bandwidth CLI. Account ID is
discovered automatically, not required from the user.

Startup OAuth: if BW_CLIENT_ID and BW_CLIENT_SECRET env vars are set,
token exchange happens during setup(). All API requests use Bearer auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds createCall and 27 other voice tools, numbers management,
and toll-free verification to the server. All specs fetched from
dev.bandwidth.com at startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test now asserts tools > 0 and validates filtering behavior directly
instead of predicting a total count that breaks when specs change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Basic Auth (BW_USERNAME/BW_PASSWORD) with OAuth2 client credentials
(BW_CLIENT_ID/BW_CLIENT_SECRET) across AGENTS.md, README.md, and
src/instructions.py. Account ID is now auto-discovered from JWT claims.

Also updates AGENTS.md to reflect new Voice API (28 tools), Numbers API,
Toll-Free Verification, and removes stale limitations (no voice tools,
no webhook registration).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
specs/ dir wasn't included when installed via uvx. Added specs as a
proper Python package with __init__.py so setuptools includes the
YAML files. Resolves spec path via module import instead of __file__.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Voice and insights both define listCalls/listCall. Exclude the insights
duplicates via per-spec exclude_tools so voice's versions win.
Also reorder specs so voice loads before insights.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setup() now mutates _config via update() instead of reassigning, so
setCredentials and _reload_authenticated_servers share the same dict.
Fixed integration test isolation — clear module state between tests.

Production mid-session flow verified: 8 → 96 tools after setCredentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AGENTS.md was referenced relative to repo root which doesn't exist
in uvx cache. Moved into specs/ package alongside express.yml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PATCHes the Bandwidth Applications API to point callback URLs at this
server's public URL. Agent calls configureCallbacks(application_id, base_url)
and webhooks are wired — no manual dashboard config needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lper

Config resource was exposing BW_CLIENT_SECRET and BW_ACCESS_TOKEN
to any MCP client reading resource://config. Now redacts sensitive
values. Also removed unused create_auth_header (Basic Auth is gone).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…saging

Agent was burning 170k tokens exploring source code to figure out how to
make a call. Instructions now give explicit step-by-step sequences:
find your number → configure callbacks → generate BXML → createCall.
Header explicitly tells agent to use tools, not read source code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tools are now always registered from OpenAPI specs, even without
credentials. Auth is checked at API call time (401), not at startup.
This eliminates the mid-session reload problem — clients always see
the full tool list.

Credentials come from env vars in the MCP config. setCredentials
remains for express registration flows but is no longer the primary
auth path. Removed _reload_authenticated_servers and requires_auth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pyproject.toml only listed packages (specs/, tools/) but not the
loose .py modules (app, servers, config, etc.). Added py-modules
list so uvx installs the complete server, not just the subpackages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Numbers spec has malformed $refs (strings instead of dicts), missing
response descriptions, and allOf misplaced inside properties. Added
three spec patching functions to _clean_openapi_spec that fix these
patterns. Numbers API now loads 343 tools successfully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
430 tools was unusable. Default now loads a task-oriented set covering
messaging, voice, lookup, MFA, number discovery, and callbacks (21 tools).
Numbers API (343 tools) is opt-in via BW_MCP_PROFILE=numbers.
Profiles are task-oriented, not API-oriented. dict.fromkeys() for dedup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setup() was running in asyncio.run() before mcp.run(), causing
"request before initialization was complete" errors and -32602 on
every tool call. Moved setup into a lifespan context manager so it
runs inside FastMCP's event loop during proper initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ofiles

Numbers API is XML-based — from_openapi sends JSON which the API rejects.
Disabled until we build a proper XML adapter. Added follow_redirects=True
to httpx clients for APIs that use 303 redirects. Profiles now cherrypick
operationIds matching the CLI command surface.

Created voice app d27b5ce6-167f-4664-9c03-c60b472f6fae on account 9901287.
Verified createCall works with +19192964738 as from number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no BW_MCP_BASE_URL is set and transport is HTTP, the server
automatically starts a Cloudflare quick tunnel. Zero user config —
callbacks just work. Production deploys set BW_MCP_BASE_URL instead.

Verified end-to-end: tunnel URL → configureCallbacks → createCall.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
respondToCallback now auto-creates call state if it doesn't exist,
allowing agents to queue BXML before the callee answers. The answer
callback checks for pre-queued BXML and serves it immediately instead
of redirecting. Instructions updated with correct sequence:
generate BXML → createCall → respondToCallback (before answer).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
custom_route must be called before mcp.run() so Starlette includes
the routes in its app. Moving from lifespan to module level fixes
404 on callback URLs. Verified: Bandwidth callback received and
returned pre-queued BXML — voice call works end to end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent needs to know exactly where to find accountId, applicationId,
from number, and answerUrl. Instructions now point to specific config
keys and give the exact answerUrl pattern. Added auto_gather guidance
for one-shot vs conversational calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eApplication

Agent can now discover phone numbers and voice applications from the
account instead of requiring pre-configured env vars. createApplication
handles the edge case where no Voice-V2 app exists — it creates one
with callback URLs auto-pointed at the server.

Instructions updated: agent reads config, discovers resources,
creates app if needed, then makes the call. Zero pre-configuration
beyond credentials.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rtup

configureCallbacks was hitting the Voice v2 JSON API which doesn't
actually update app callback URLs. Switched to Dashboard XML API
(GET current app, PUT with updated URLs). Server auto-configures
voice app callbacks on startup when BW_VOICE_APPLICATION_ID and
BW_MCP_BASE_URL are set.

Added gatherUrl to auto-generated Gather BXML so Bandwidth knows
where to POST speech results. Conversation flow verified: greeting
→ speech capture → response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop requirements.txt and dev-requirements.txt in favor of pyproject.toml
with [project.optional-dependencies]. Update CI to pip install ".[dev]".
Remove root AGENTS.md (stale duplicate of src/specs/AGENTS.md). Clean up
5 unused imports across source files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants