Qvasa MCP Server — Security and Integration Reference
This article describes the Qvasa Model Context Protocol (MCP) server: how AI agents connect to it, how access is authenticated and scoped, exactly which tools are exposed, and how every tool call reaches your Qvasa data. It is written for security reviewers evaluating the integration before connecting it.
At a glance. Toolkit version 2026-06-15.user_access_api_attribute_aliases_filter_validator_v1 · Connector v0.6.1 · Transport: Streamable HTTP / JSON-RPC 2.0 · Auth: OAuth 2.1 + PKCE.
Contents
Overview
Architecture
Authentication
Token storage & validation
Authorization & scoping
Scope reference
Multi-tenancy & isolation
The MCP tools
How tools reach your data
The client connector
Token lifecycle
Auditing & observability
Data handling & limits
Independent review & download
1. Overview
The Qvasa MCP server lets an AI agent (e.g. Claude) work with your Qvasa customer-support analytics on a user's behalf — running metrics, inspecting tickets, and managing dashboards and AI classifiers — strictly within the permissions that user already holds in Qvasa.
It is implemented natively inside the Qvasa Rails application using the official Model Context Protocol Ruby SDK. The agent talks to a single HTTPS endpoint; there is no separate hosted service, queue, or data store introduced by the MCP layer. It reuses the same application, authorization, and database paths as the rest of Qvasa.
Security posture in one line — every MCP request is authenticated, scoped to a single account, intersected against the calling user's live permissions, restricted to a hard endpoint allowlist, and audited. There is no privileged bypass path introduced by the MCP.
2. Architecture
The integration has two halves:
Hosted MCP server: a single endpoint,
POST /mcp, served by the Qvasa application. It is stateless (each request is fully self-contained), speaks JSON-RPC 2.0 over the MCP Streamable HTTP transport, and is the sole authority for authentication, scope enforcement, validation, and tenant isolation.Client connector: a small, open, reviewable package that runs inside the user's AI client (Claude Desktop, Claude Code, Cursor). It is a thin proxy: it holds the user's credential locally and forwards calls to the hosted server. It enforces no business logic of its own (see section 10).
Discovery follows the standard OAuth protected-resource pattern. An unauthenticated call to /mcp returns 401 with a WWW-Authenticate header that points clients to the metadata endpoints:
GET /.well-known/oauth-protected-resource # RFC 9728: resource metadata + supported scopes GET /.well-known/oauth-authorization-server # RFC 8414: authorize / token / registration endpoints, PKCE
3. Authentication
The MCP accepts two credential forms, both of which resolve to the same backing token record:
OAuth 2.1 (hosted connector path). Authorization-code flow with PKCE required, implemented with Doorkeeper. The agent obtains a bearer access token and presents it as
Authorization: Bearer <token>on every/mcprequest. Refresh tokens are supported and rotated.Personal access token (CLI path). A user-generated token (prefix
qva_uat_) for clients that pass a static credential. Same authorization and scoping rules apply.
Authentication and the foundational permission gate run as before_actions on the MCP controller, so no tool code executes until the caller is identified and authorized.
4. Token storage & validation
Raw tokens are never persisted. The plaintext token is shown to the user exactly once at creation.
What is stored is an HMAC-SHA256 digest of the token (keyed by a secret held only in the server environment), plus a short non-sensitive prefix for display (e.g.
qva_uat_1a2b…). The digest column is uniquely indexed.Validation hashes the presented token and looks it up by digest among active tokens (not revoked, not expired). A miss is an authentication failure.
OAuth access tokens are managed and validated by Doorkeeper independently of the digest path.
Implication — a database disclosure does not expose usable credentials: stored digests are not reversible to tokens, and the HMAC secret is not in the database.
5. Authorization & scoping
Access is least-privilege by construction. Every request is authorized through three independent layers, and is permitted only if all of them allow it:
Token scopes. Each token carries an explicit set of capability scopes (a bitfield). A token only ever has the scopes it was minted with.
The user's live permissions. At request time the token's scopes are intersected with the calling user's current account permissions. If a user's permission is later revoked in Qvasa, their tokens immediately lose that capability, even if the scope bit is still set on the token.
Account-level enablement (foundational gate). Before any scope is even consulted, three settings must hold: the feature is enabled for the account at the platform level (
can_enable_user_access_tokens), the account has opted in (scoped_user_access_tokens_enabled), and the user is permitted to use access tokens (can_manage_user_access_tokens). Any failure denies the request before it reaches a controller.
Baseline vs. above-baseline
Reading individual ticket detail (can_inspect_objects) and conversational content (can_access_conversational_data) are opt-in capabilities. The broad read umbrella (can_read_all_endpoints) deliberately does not auto-grant ticket-detail inspection, which is a separate, explicit scope. Writes always require an explicit write scope; nothing is writable by default.
6. Scope reference
The tables below reflect the scopes that can be granted to a token today. Two umbrella scopes (can_read_all_endpoints, can_write_all_endpoints) act as convenience grants over the granular read/write scopes; the remaining scopes can be granted individually for finer-grained tokens.
Umbrella scopes
Scope | Grants |
| Grants every Read scope listed below |
| Grants every Write scope listed below |
Additional Read Access (Above Baseline)
Scope | Capability | What it grants |
| Inspect ticket details | Drill into individual ticket details (metadata, tags, assignments, history) |
| Access conversations | Read message bodies and transcript content |
Ability to Manage Qualitative Questions (AI Classifiers)
Scope | Capability | What it grants |
| Create AI classifiers | Create new custom basic_score or two_choice questions |
| Edit inactive AI classifiers | Edit response options while the classifier is inactive |
| Activate & backfill AI classifiers | Submit for AI review, activate, run backfill |
| Cancel AI classifier backfills | Cancel an in-progress backfill job |
| Edit active AI classifiers | Edit response options on an active classifier |
Ability to Leverage On-Demand AI Insights
Scope | Capability | What it grants |
| Run AI insights jobs | Run extract_themes or knowledge_base_article on a ticket set |
Ability to Manage Generative Definitions (Generative AI Workflows)
Scope | Capability | What it grants |
| Create generative AI workflows | Create new drafts and clone existing definitions |
| Publish generative AI workflows | Publish a draft (one-way; locks core fields) |
| Edit published generative AI | Edit mutable fields of a published definition |
| Wire generative AI to classifiers | Wire a published definition to a classifier response option |
| Wire nested AI classifiers | Wire a nested classifier to a parent response option |
Ability to Manage Scheduled Report Exports
Scope | Capability | What it grants |
| Scheduled report exports | Create and schedule recurring dashboard exports |
Ability to Manage Workflow Condition Alerts
Scope | Capability | What it grants |
| Workflow condition alerts | Build threshold conditions on widgets and fire alerts |
Ability to Manage Standard Threshold Alerts
Scope | Capability | What it grants |
| Standard threshold alerts | Manage the legacy AlertCondition queue-based alerts |
Ability to Acknowledge Alerting Incidents
Scope | Capability | What it grants |
| Acknowledge alert incidents | Mark threshold-alert incidents as recognized |
Ability to Access Open Dashboards
Scope | Capability | What it grants |
| Read open dashboards | List and fetch open (team-shared) dashboards |
| Build & edit open dashboards | Create and edit widget/row layout on open dashboards |
Ability to Access Private Dashboards
Scope | Capability | What it grants |
| Read private dashboards | List and fetch private dashboards |
| Build & edit private dashboards | Create and edit widget/row layout on private dashboards |
Ability to Access Agent Dashboards
Scope | Capability | What it grants |
| Read agent dashboards | List and fetch agent dashboards |
| Build & edit agent dashboards | Create and edit widget/row layout on agent dashboards |
Note — running metric queries (single-number stats, charts, donuts, heat-maps) and maintaining a personal-dashboard "memory" are baseline capabilities of any token and do not require an additional scope. Everything above the baseline is opt-in per the scopes listed here.
7. Multi-tenancy & isolation
Qvasa is multi-tenant; account isolation is enforced at every layer the MCP touches:
Every token and every session record
belongs_toexactly one account.A token's user is validated to belong to the token's account; a mismatch is rejected.
Sessions are validated to share the user and account of their token.
All data access flows through the standard Qvasa application queries, which are scoped by
account_id. There is no MCP code path that reads across accounts.
8. The MCP tools
The server exposes a deliberately small "portal" of eight tools. Most Qvasa capabilities are reached not by a dedicated tool but through a search → guide → call pattern over the catalog, which keeps the tool surface minimal and auditable.
Tool | What it does | Reaches |
| Returns a small orientation packet describing how to use the API. | Read-only guidance content. |
| Searches the tool / metric / filter catalog by phrase (primary discovery verb). | Read-only catalog. |
| Fetches the full guide (prose, paths, required scopes) for one named tool. | Read-only guidance content. |
| Fetches one workflow recipe (an ordered tool sequence) by intent id. | Read-only guidance content. |
| Runs a metric ( | Analytics over your ticket data, scoped to the account. |
| Generic dispatcher to a Qvasa API endpoint by method + path (the workhorse). Used after discovery to execute a specific capability. | Allowlisted only: paths under |
| Sends a bug report / friction note back to the Qvasa team (the escape hatch). | Writes a feedback record only. |
| Compatibility no-op on the hosted connector (the OAuth connection is the session). Present for parity with the local package. | None. |
On qvasa_call_api — the generic dispatcher cannot reach arbitrary application routes. It is hard-restricted to the /user_access/api/* namespace and the five standard verbs; anything else is refused before dispatch. Every endpoint it reaches enforces its own scope and validation, and the dispatcher grants no additional authority.
9. How tools reach your data
A tool call does not open a new network connection or use a privileged internal channel. The server translates the tool call into an in-process request that re-enters the same Qvasa controller stack used by the rest of the application, carrying the caller's credentials. This means:
The identical authentication, scope checks, validation, and account scoping apply, so there is no parallel, less-guarded code path.
The dispatcher enforces the
/user_access/api/*path allowlist and verb allowlist before any controller runs.Reads are the default; any write requires an explicit write scope (see section 6).
HTTP status codes from the underlying endpoint (e.g.
403scope denial) are surfaced back to the agent unchanged.
10. The client connector
The downloadable connector (the package distributed in the .mcpb bundle, see section 14) is a thin Python proxy (Python 3.11+, run via uv). Its security-relevant properties:
Single destination. It makes outbound HTTPS calls only to the configured Qvasa host (default
https://www.qvasa.com, overridable for staging or self-hosted). There is no telemetry, analytics, or third-party / model-provider call of any kind.Mirrored allowlist. It enforces the same
/user_access/api/*path prefix and the same verb allowlist as the server, for defense in depth on both ends.Credential handling. The user's access token is supplied via an environment variable. In Claude Desktop it is declared as a sensitive config field and stored in the OS keychain. The short-lived session is held in process memory only and is never written to disk.
No secret logging. The connector does not log credentials or request / response payloads; errors are returned as structured objects with recovery hints.
No independent authority. The connector interprets no scopes and makes no authorization decisions; Qvasa is the sole authority.
11. Token lifecycle
Credential | Lifetime | Notes |
OAuth access token | 8 hours | Short-lived bearer; refreshed via rotating refresh token. |
OAuth refresh token | Rotated on use | Prior refresh token is revoked when a new one is issued. |
Personal access token (UAT) | 90 days default, 365 max | User-chosen expiry within bounds; revocable at any time. |
Session token | 24 hours | One active session per token; pins the toolkit version for the session. |
Revocation is comprehensive. Disconnecting an OAuth connection (or revoking a token) runs in a single transaction that revokes the OAuth access and refresh tokens and their grants, revokes the backing token record, and invalidates all active sessions, so in-flight agent sessions stop working immediately.
12. Auditing & observability
Token lifecycle audit trail. Creation, permission changes, revocation, expiry, and deletion are recorded as events (with the acting user and token metadata). The audit write is fail-loud: if it cannot be recorded, the surrounding operation is rolled back.
Per-request records. Metric and export requests are persisted with the account, user, token, session, status, and timestamps, providing an attributable record of what was run.
Last-used tracking. Tokens and sessions track last-used time (throttled to limit write churn).
Error reporting. Unexpected tool errors raise an internal alert with a support code that is also surfaced to the agent, so issues are traceable end-to-end.
13. Data handling & limits
Through the MCP a sufficiently-scoped token can reach the same data the user can already see in Qvasa: ticket metadata and analytics, and, only with the explicit conversational scope, message and transcript content. Nothing beyond the user's own visibility is exposed.
Stated plainly, for completeness — abuse controls today are concurrency caps (e.g. one active session per token; one active bulk export per token) rather than a global request-rate limit, and the MCP endpoint does not currently enforce IP allowlisting. Access is, however, fully bounded by per-request scope checks, the live-permission intersection, account isolation, and short credential lifetimes described above. We are happy to discuss additional network controls for your deployment.
14. Independent review & download
The client connector is provided for your team to review directly. The download is an MCP bundle (.mcpb), a standard ZIP archive, containing the connector's full source:
Qvasa MCP connector bundle — qvasa-mcp.mcpb · connector v0.6.1 · Python 3.11+ (run via uv).
To inspect it:
unzip qvasa-mcp.mcpb -d qvasa-mcp # Contents: # manifest.json : MCP/DXT manifest (declares the sensitive token field, default host) # pyproject.toml : package metadata and dependencies # uv.lock : fully pinned dependency lockfile # src/qvasa_mcp/** : connector source (tools, client, allowlist) # README.md : usage and configuration
Reviewing the source confirms the properties in section 10: a single outbound destination, the mirrored endpoint allowlist, in-memory-only session handling, and no telemetry. Dependencies are pinned in uv.lock for reproducible installs.
