The scope grammar
Every Halo MCP token carries a list of scopes the user approved at consent time. Scopes are space-separated strings with these shapes:| Scope | Grants |
|---|---|
actions:* | Every non-destructive action on every connected integration. Also implies docs:read, setup:read, and data:read. |
actions:<integration>:* | Every non-destructive action on one integration (e.g. actions:hubspot:*). |
actions:<integration>:<action> | One specific (non-destructive) action (e.g. actions:hubspot:create_deal). |
docs:read | The proxied search_docs and read_doc tools that hit the public Halo docs. |
setup:read | The read-only setup helpers setup_get_install_snippet and setup_get_jwt_sample. Lets a connected client (Cursor, Claude Desktop) walk the customer through installing the widget. |
data:read | All read-only workspace-data tools. Records: data_find_user, data_search_users, data_search_leads, data_find_company, data_search_companies, data_search_tickets, data_get_ticket, data_search_transcripts, data_get_transcript, data_list_negative_feedback. Product-insight tools: data_search_events (SDK behaviour search), data_top_pages_before_chat, data_top_unresolved_questions, data_user_journey, data_low_sentiment_conversations. Ask-AI-parity retrieval + analytics: data_search_knowledge (Voyage embed + Postgres match_knowledge hybrid + Voyage rerank, the same pipeline that powers Ask AI thinking mode), data_query_integration (recency cuts over synced integration data), data_query_metrics (KPI snapshots), data_query_activity_time_series (bucketed DAU / new users / conversations / tickets), data_get_hubspot_pipeline_stages, data_query_hubspot_deals, data_query_hubspot_engagements, data_query_stripe, data_lookup_stripe_customer, data_list_automations, data_get_automation_performance, data_list_automation_recipients, data_query_email_engagement_overview, data_web_search, data_web_fetch. The MCP client can answer “who is this customer”, “what tickets are open”, “what did we tell Acme about onboarding last quarter”, and “how has churn changed in the last 30 days” without touching the dashboard. |
Destructive actions are not exposed via MCP
Halo’s action registry flags destructive operations (refunds, deletes, irreversible writes) withdestructive: true. Those tools are intentionally not surfaced through MCP, regardless of the token’s scopes. The gate (scopeAllowsDestructive) returns false unconditionally.
This is a deliberate trade-off: an external MCP client (running on a teammate’s laptop, a CI box, an AI coding tool) sits at one extra layer of trust away from the operator who’s already in the dashboard. We’d rather close that path entirely than ask every operator to reason about whether this particular destructive call is safe at consent time. If you need destructive operations from an automation, drive them through a Halo agent invocation in the dashboard or a server-side workflow with its own guardrails.
If the policy ever changes, it flips in lib/integrations/mcp-oauth/scopes.ts — there’s a single chokepoint, not a scattered set of checks.
The three-layer gate
A non-destructive integration tool call has to clear three independent checks before it runs. Each layer is enforced server-side every call (no caching, no client-supplied claims):Workspace integration mode
The integration’s Actions mode must be on (toggled at the top of the integration’s detail page). If you flip it off, every tool from that integration disappears from MCP
tools/list immediately.Per-agent action toggle
The action must be enabled on the org’s Ask AI agent (the same agent the dashboard’s Ask AI surface uses). Same setting that gates the action when called from a chat conversation.
action_executions row with the rejection reason (scope_denied, destructive_blocked, integration_disabled, agent_disabled, etc.) so you can answer “why didn’t this MCP client manage to do X” from the audit log instead of from server logs.
Binding a customer to a session
Tokens are org-scoped. To act on behalf of a specific customer (look up their data, file a ticket on their conversation, etc.), the client passes the binding inline on each call via the JSON-RPC_meta envelope:
bound_email instead of bound_end_user_id if your client only knows the email. Halo re-resolves the binding on every call against your org’s customer table; the binding is never trusted across calls and never trusted across orgs.
Audit trail
Every MCP call writes anaction_executions row with:
- The acting actor (
mcp_client) and the user who granted the token (grantedByUserId) - The token id (so you can correlate every call from one token without dereferencing the token itself)
- The bound customer (when present), re-resolved server-side
- The full input args
- The outcome (success, validation failure, scope denial, rate limit, integration error)
- A timestamp + duration
Token lifecycle
| Step | Detail |
|---|---|
| Issue | At /api/mcp/oauth/token after PKCE-verified code exchange. Returns access_token (1 hour TTL) + refresh_token (long-lived, rotates on use). |
| Refresh | Standard grant_type=refresh_token. Rotation: the old refresh row is revoked atomically as the new pair is issued. Refresh-token replay revokes the entire derived chain. |
| Downscope | refresh_token requests can include scope= to drop scopes. The new scopes must be a subset of the current ones; you can never upscope on refresh. |
| Revoke | POST /api/mcp/oauth/revoke with the access or refresh token. Idempotent and always returns 200 (per RFC 7009 §2.2 to prevent token-shape probing). |
| Auto-cleanup | Inactive clients (never issued a token within 30 days of registration) are auto-pruned by a nightly cron. Active clients are kept indefinitely. |
Recommended scope sets
Pick the smallest set that covers your use case:| Use case | Scopes |
|---|---|
| Help a customer install the widget from inside Cursor / Claude Desktop | setup:read docs:read |
| Read-only product-insight surface (search users, tickets, conversations, negative feedback) | data:read docs:read |
| Look up customer + conversation data + take non-destructive actions | actions:* data:read |
| Documentation search only | docs:read |
| Restricted to a single integration | actions:hubspot:* |
| Restricted to a single action | actions:hubspot:create_deal |