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-uiThe 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:
getBuckyMcpGuidedescribes the surface, how to start OAuth by calling a gated tool, and safety rules.explainFeasibilityStatusexplains 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:
status | Meaning |
|---|---|
cited | Extracted from the bylaw. |
cited_conditional | Resolved from a conditional rule that applies to this site. |
conditional_unresolved | Conditional tiers exist; none resolve for this site. Unknown, not unlimited. |
no_limit_in_bylaw | The bylaw sets no zone-level limit. A real answer. |
unverified_default | A cached zoning value with no citation behind it. Indicative only. |
not_extracted | Never 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)
- Add remote MCP server URL:
https://buckybuild.com/api/mcp - Call an authenticated tool (for example
getFeasibilitySnapshot). Complete the browser Bucky sign-in your client presents. Do not disconnect the connector first. - Protected resource metadata:
https://buckybuild.com/.well-known/oauth-protected-resource/api/mcp - 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.pdfInteractive 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_TOKENMissing, 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/mcpThe 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