BuckyDocs
MCP

MCP Integrations

Reach Bucky's MCP endpoint over Streamable HTTP and verify it locally.

Bucky's MCP surface is a route inside the web app, not a separate service: it is POST /api/mcp, served by the same deployment as everything else. The transport is Streamable HTTP, stateless — no session id. It serves MCP 2026-07-28 and keeps a stateless compatibility leg for 2025-era clients. GET and DELETE on the endpoint answer 405.

Named desktop and editor client configurations are still pending transport compatibility testing; do not translate this endpoint into a stdio or SSE configuration.

Agents can discover the server before connecting through the public /.well-known/mcp/server-card.json document. It advertises the same server name and version as the runtime, MCP 2026-07-28, the /api/mcp Streamable HTTP endpoint, and dynamic tools. The card marks tools as dynamic because tools/list is the runtime authority; the catalog itself is the same for anonymous and authenticated callers.

Requirements

Use Node 24 or newer and pnpm 11.8.0 from a Bucky checkout. The route needs the app's usual environment, so run the app itself:

pnpm nx dev bucky-ui

The examples below assume http://127.0.0.1:3000; substitute the port the dev server prints if it differs.

Verify the current protocol

MCP 2026-07-28 uses a per-request _meta envelope and server/discover; it does not require an initialization handshake:

curl --fail --silent http://127.0.0.1:3000/api/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --header 'Mcp-Method: server/discover' \
  --data '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"}}}}'

The result lists 2026-07-28 in supportedVersions, includes the tools capability, and carries resultType: "complete".

The endpoint also supports 2025-era initialize, ping, and tools/list. For that compatibility leg the spec requires clients to accept both application/json and text/event-stream. Omitting either gets a 406, not a protocol answer:

curl --fail --silent http://127.0.0.1:3000/api/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

The response may be an SSE data: frame, which is valid Streamable HTTP.

List tools

curl --fail --silent http://127.0.0.1:3000/api/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

The result's tools array is the runtime authority. Regenerate the generated tool reference with pnpm generate-mcp-reference when bundle tools change.

tools/list always returns the full catalog. Execution stays gated: calling an authenticated tool without a Bucky access token returns HTTP 401 and WWW-Authenticate, which is what starts browser OAuth. An expired or invalid bearer also 401s — including on tools/list — so the client refreshes instead of silently falling back to the two anonymous tools. Claude.ai ToolSearch only indexes names from tools/list; hiding gated tools until a token is already on the wire means they are never called and OAuth never starts.

Origin

Requests carrying an Origin header are validated against an allowlist and rejected with 403 if it does not match — required by the transport spec to prevent DNS rebinding. Requests with no Origin are allowed, which is the normal case: non-browser MCP clients do not send one. Add browser origins via the MCP_ALLOWED_ORIGINS environment variable (comma-separated).

Call the public zero-cost tools

Anonymous callers can still invoke two static tools without a token:

  • getBuckyMcpGuide describes the surface, how to start OAuth by calling a gated tool, and safety rules.
  • explainFeasibilityStatus explains one Bucky coverage or fact status and its safe next action.

tools/list also advertises the authenticated tools. Call one of those to start sign-in; do not disconnect the connector first.

Neither public tool queries a database, geocoder, AI model, or third-party API, so this MVP does not require Redis or another shared spend limiter.

curl --fail --silent http://127.0.0.1:3000/api/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"getBuckyMcpGuide","arguments":{}}}'
curl --fail --silent http://127.0.0.1:3000/api/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --data '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"explainFeasibilityStatus","arguments":{"status":"not_extracted"}}}'

Call address feasibility with authentication

getFeasibilitySnapshot requires a valid bearer token because resolving an address may use cost-bearing infrastructure. It returns the lot, its zone and permitted uses, and the building envelope in one call.

Every envelope value arrives with its own status, and a null value means nothing without it:

statusMeaning
citedExtracted from the bylaw.
cited_conditionalResolved from a conditional rule that applies to this site.
conditional_unresolvedConditional tiers exist; none resolve for this site. Unknown, not unlimited.
no_limit_in_bylawThe bylaw sets no zone-level limit. A real answer.
unverified_defaultA cached zoning value with no citation behind it. Indicative only.
not_extractedNever extracted for this zone. Unknown.

no_limit_in_bylaw and not_extracted both carry value: null and mean opposite things. Reporting the second as "no limit" is the failure mode this shape exists to prevent.

When an address falls outside coverage the call still succeeds, with answered: false and a coverage block saying why — planned_not_live, unseeded_place, geocode_failed, and so on. coverage.eligibilityKnown is false when we could not determine eligibility at all, which is distinct from determining that a place is not covered.

Call the authenticated tier

Workflow bundles require a real Bucky account. OAuth browser sign-in is the happy path for Claude Desktop, ChatGPT, and Cursor. Bearer JWT paste is a Cursor/scripting fallback (tokens expire in about one hour).

OAuth (Claude Desktop, ChatGPT, Cursor)

  1. Add remote MCP server URL: https://buckybuild.com/api/mcp
  2. Call an authenticated tool (for example getFeasibilitySnapshot). Complete the browser Bucky sign-in your client presents. Do not disconnect the connector first.
  3. Protected resource metadata: https://buckybuild.com/.well-known/oauth-protected-resource/api/mcp
  4. Authorization server metadata: https://buckybuild.com/.well-known/oauth-authorization-server

Call getBuckyMcpGuide over MCP for the in-band setup summary and first-project checklist.

CLI (agents use --profile agent; humans run login once)

bucky --profile agent auth login
bucky --profile agent agent check
bucky --profile agent projects create --name "Demo" --address "4170 Sophia St, Vancouver BC"
bucky --profile agent analysis run <project-id>
bucky --profile agent analysis wait <project-id>
bucky --profile agent reports export <project-id> --out report.pdf

Interactive auth login opens the same browser identity as the web app. Password, stdin tokens, and environment variables remain supported for automation.

Bearer fallback (Cursor scripting only)

Keep tokens out of command history by reading without echo:

read -r -s 'BUCKY_MCP_TOKEN?Supabase user access token: '
export BUCKY_MCP_TOKEN

curl --fail --silent http://127.0.0.1:3000/api/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --header "Authorization: Bearer ${BUCKY_MCP_TOKEN}" \
  --data '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"getFeasibilitySnapshot","arguments":{"address":"4170 Sophia St, Vancouver BC","source":"zoning_lookup"}}}'

unset BUCKY_MCP_TOKEN

Missing, malformed, expired, or invalid bearer tokens are rejected with -32010. A tool that fails on its own terms returns a normal result with isError: true; an unexpected server-side failure returns -32603 with no detail, and the real error is written to stderr.

Bearer tokens expire in about an hour. OAuth refresh is the documented client path.

Submit human-reviewed feedback

Authenticated callers also receive submitBuckyFeedback. Call it only when the user explicitly asks to send feedback, report a problem, suggest an improvement, or praise something. The first call returns resultType: "input_required" with a form elicitation request. A compatible MCP client presents that form to the human and retries the same tool call with the accepted response.

Nothing is persisted when the form is declined or cancelled. Accepted feedback is validated and scrubbed for common personal-data and credential shapes before it is stored in Supabase. Generate a UUID for submissionId and reuse that UUID when retrying the same report; duplicate submissions are treated as a successful idempotent retry.

This tool is not an agent telemetry channel. Agents must not invoke it silently or infer feedback from a failed tool call. Agent-observed operational friction continues to use the internal papercut signal.

Before deploying the tool, apply supabase/migrations/20260729223000_generalize_feedback_for_mcp.sql. The migration adds the narrowly scoped RLS policy for explicit MCP feedback and extends the admin feedback view. If the write fails, the tool returns success: false and tells the caller to retry with the same submissionId; it does not silently acknowledge a dropped report.

Test seam

Vitest resolves the include globs relative to its working directory, so run this from the app, not the repo root:

cd apps/bucky-ui
pnpm exec vitest run --config vitest.config.ts app/api/mcp server/mcp

The route suite drives the official v2 SDK through POST with a stubbed tool registry, covering both protocol eras, header/body validation, the body cap, tools/list, tools/call, per-tool auth, and error sanitisation. The registry suite loads the canonical tool graph unmocked and asserts every annotation and auth boundary. Feedback tests cover elicitation, decline/cancel, validated acceptance, idempotency, provenance, and failed-write behavior. The bundle suite covers coverage honesty, redaction, raw-address non-retention, attribution, and the cache.

Last updated on

On this page