Skip to content

Connect external agents

External agents let a signed-in user connect their own AI client (an MCP client such as a desktop or CLI agent) to your application. Calls run in the user’s open tab, with the same skills, transports and approval dialogs as the Bunkhouse pane. The host’s credentials never leave that tab; only validated tool inputs, results and compact action outcomes travel through Bunkhouse. Bridge calls use no hosted model and no credits.

The same setting also publishes the embed’s skills to in-page agents through WebMCP (document.modelContext).

External agents are off by default. In the dashboard, open the site and find External agents:

  1. Check Allow external agents. Turning it off later blocks new connections, MCP calls and WebMCP publication immediately, but keeps existing grants: while off, MCP and token requests get HTTP 503 temporarily_unavailable, and re-enabling makes the grants usable again without new consent (each user must reconnect their tab).
  2. Choose Skills exposed to external agents:
    • Trusted: skills from your hints file and published discovery cache. WebMCP tools your page declares itself are callable over the relay, but are never republished.
    • All live skills: also adds observed API calls and page controls discovered live in the user’s tab.
  3. Optionally set a Consent page URL. It must be on one of the site’s allowed origins and load the embed with your usual identity integration. When empty, users land on the first allowed origin’s root (https://app.example.com/). Path and query are kept, so hash-routed apps can pick a dedicated page.
  4. Copy the MCP server URL, https://api.bunkhouse.ai/mcp/<public key>, and give it to users.

You do not implement OAuth on your host, add a CSP domain, or change your identity integration. Bunkhouse is the authorization server; your signed embed session identifies the user.

Rotating the site’s public key changes the MCP URL and revokes every grant for the site.

  1. The user adds the MCP URL to their client. The client discovers Bunkhouse’s OAuth metadata, registers (dynamic registration or a client metadata document) and starts an authorization-code flow with PKCE (S256).
  2. Bunkhouse redirects the browser to your consent page. The embed opens its Agents view showing the client’s name (marked unverified), client ID, callback host, your site and the requested scopes (skills:read, skills:write). A loopback-only client gets a warning. The user must be signed in; otherwise the view asks them to sign in and retry.
  3. On Allow, the browser opens the client’s callback in a new window and the original tab becomes the connected executor: “Connected to . Keep this tab open.” If the popup is blocked, a Continue link appears instead.

Clients must send the OAuth state parameter (1–1024 characters), a PKCE S256 code challenge and the resource parameter set to the MCP URL. Bunkhouse rejects authorization requests without them; state is stricter than base OAuth and MCP, so clients that omit it cannot connect. Clients register through dynamic client registration (/register, public or client_secret_basic) or an HTTPS client ID metadata document. Authorization server metadata is at /.well-known/oauth-authorization-server on the API origin. Redirect URIs must be HTTPS or a literal loopback address (localhost, 127.0.0.1, [::1], any port); custom URI schemes are not supported.

Access tokens last 5 minutes. Refresh tokens rotate on every use, and reusing a spent refresh token revokes the grant. A grant lasts 30 days from last use, at most 90 days.

Tool calls only run while a connected tab is live. With no live tab, calls fail immediately with no_live_tab and a link to reconnect. Reloading the page reconnects the same tab automatically. A duplicated tab cannot take over a live connection; the user chooses Use this tab in another tab to move it explicitly.

Actions that drive page controls need the tab visible. A hidden tab fails them with tab_not_visible; ask the user to focus the connected tab. API and WebMCP actions keep working in the background. Browsers may still freeze or discard background tabs.

The MCP server exposes search_skills, describe_skill and execute. Each execute call carries a client-chosen UUID requestId:

  • Retrying with the same requestId and code never runs the code twice; it returns the existing state or result.
  • If the call is waiting on an approval, the result is pending (approval_pending or execution_pending). Call again with the same requestId to resume.
  • Results include per-call action outcomes, paged with outcomeCursor. unknownTail means effects may have happened beyond the confirmed outcomes; do not blindly replay writes.
  • Results are kept for 24 hours, then return result_expired. Intentional new work uses a new requestId.

Scopes are enforced per action: reads need skills:read, writes and unknown effects need skills:write. A missing scope returns HTTP 403 with an insufficient_scope challenge; after the user grants it, retrying the same requestId returns the original result without running it again.

Writes show the normal approval dialog, labelled with the requesting client. If the site allows run auto-approval, the user can choose to auto-approve these action kinds while that client is connected. That choice lasts only for the connected tab’s session with that client and ends on disconnect, reload, sign-out or revocation. Identical repeated writes always ask again. In-page WebMCP callers are never offered auto-approval.

When enabled, the embed registers its exposed skills as WebMCP tools named bunkhouse.<hash> without opening the pane. Browsers without native WebMCP get a bundled polyfill, which needs a secure context and Chrome 126, Firefox 126 or Safari 18. An existing document.modelContext is never replaced. Without WebMCP support, the MCP relay still works.

  • Users revoke their own connections in the embed’s Agents view; owners revoke any grant in the dashboard’s Connected agents table. Revocation stops tokens and cancels running calls.
  • Disconnect in the tab ends only that tab’s connection; the grant stays.
  • Calling resetSession() on sign-out disconnects the tab immediately. If your host does not call it, sign-out is only detected at the next identity renewal (up to the 15-minute session lifetime).