Using the Qvasa MCP Tools Over Plain HTTP (No MCP Client Required)
This is a technical reference for AI agents and developers who want to use Qvasa's MCP capabilities without installing the Qvasa MCP connector. Every tool the hosted MCP server exposes is a thin wrapper over one HTTP endpoint in Qvasa's agentic API, so anything you can do through the MCP you can do with a User Access Token and an HTTP client. This article gives you the exact endpoints, headers, request bodies, and response shapes for the portal tools, plus how to reach everything else through the API's built-in discovery.
If you are an AI agent reading this: the flow is token → session → authenticated calls. Every response you get back carries an explanation and usually a next_step with the exact method and path to call next. Read them; they are written for you.
At a glance
Base URL |
|
API namespace |
|
Credential | A User Access Token, prefix |
Session | A 24-hour session token, prefix |
Alternative transport | JSON-RPC 2.0 to |
Rate limits | 120 calls per minute per session, 20 per minute for expensive calls (metric kickoffs, AI jobs); a |
Before you start
You need a User Access Token. A human generates one in Qvasa under User Settings → User Access Tokens; the raw token is shown exactly once. See User Access Tokens and the Qvasa MCP for enabling the feature, granting the permission, and choosing scopes.
A token can never do more than its owning user can do in Qvasa. Scopes are intersected with the user's live permissions on every request, so a
403is a permissions boundary to report to the user, not a bug to retry.Send
Content-Type: application/jsonon requests with a body. All examples below usecurl; any HTTP client works.For the security model behind all of this (token storage, scopes, tenancy, auditing), see Qvasa MCP Server — Security and Integration Reference.
Step 1: Create a session with your token
Qvasa uses two-token authentication. The long-lived User Access Token proves who you are; a short-lived session token authorizes every tool call. POST /user_access/api/sessions is the only endpoint that accepts the User Access Token alone, and it is the required first call.
curl -X POST https://www.qvasa.com/user_access/api/sessions \ -H "Authorization: Bearer qva_uat_YOUR_TOKEN" \ -H "Content-Type: application/json"
A 200 response has this shape (the toolkit object is large and is abbreviated here):
{
"session_id": 1234,
"session_token": "qva_uas_...",
"session_token_expires_at": "2026-09-18T18:48:33Z",
"toolkit_version": "2026-09-16.user_access_api_qvasa_trigger_summary_multi_line_target_v1",
"toolkit": {
"description": "...",
"headers_required": {
"Authorization": "Bearer <your qva_uat_... token>",
"X-User-Access-Session": "<the session_token returned with this manifest>"
},
"request_conventions": { "strict_bodies": "...", "batch": "..." },
"tools": [ { "name": "sessions", "method": "POST", "path": "/user_access/api/sessions", "description": "...", "paths": [...], "required_scopes": [...] }, ... ],
"session_lifecycle": { ... }
},
"capability_profile": {
"shape": "full_authoring",
"summary": "...",
"what_you_can_do": [...],
"what_you_cannot_do": [],
"next_step": { "method": "GET", "path": "/user_access/api/dynamic_dashboards", "description": "..." }
}
}
What to keep from this response:
session_token: send it asX-User-Access-Sessionon every subsequent call. It expires after 24 hours.toolkit.tools: the manifest of tool groups this token can actually call, already filtered to the token's effective scopes and the user's permissions. A group that is not listed is unavailable to you. Do not memorize this list across sessions; fetch it fresh each time.capability_profile: your boundaries in plain language.shapeisfull_authoringfor users who can build metrics from scratch, ordashboard_scopedfor users limited to dashboards they were granted.
Session rules worth knowing:
One active session per token. Creating a new session invalidates the previous one. To refresh an existing session instead, send a body of
{"attach_to_session_id": <session_id>}.Sessions pin the toolkit version. When Qvasa ships an agent-visible change, existing sessions return
401witherror_code: "session_stale". Recover by POSTing/sessionsagain and re-reading the manifest.Revoke a session with
DELETE /user_access/api/sessions/:session_id(token header only; returns204).
If you call /sessions with no Authorization header you get 401 with error_code: "missing_user_access_token". If the token is invalid, expired, or revoked you get 401 with the message You must provide a Bearer token to access this resource. UAT401.
Step 2: Send both headers on every other call
Every endpoint other than /sessions requires both headers. Missing the session header returns 401 with error_code: "missing_session_token". A good first authenticated call is GET /user_access/api/me, which tells you which account and user the token belongs to and, critically, which scopes are actually effective:
curl https://www.qvasa.com/user_access/api/me \ -H "Authorization: Bearer qva_uat_YOUR_TOKEN" \ -H "X-User-Access-Session: qva_uas_YOUR_SESSION_TOKEN"
{
"account": { "id": 2, "name": "acme" },
"user": { "id": 179, "email": "[email protected]", "name": "Agent Owner" },
"token": {
"id": 23,
"name": "Claude MCP",
"expires_at": "2026-12-16T18:46:53.401Z",
"granted_scopes": [ "can_read_all_endpoints", "can_write_all_endpoints", "can_inspect_objects", ... ],
"effective_scopes": [ "can_read_all_endpoints", "can_write_all_endpoints", "can_read_open_dashboards", ... ],
"scopes_filtered_by_user_permissions": [ "can_inspect_objects", ... ]
},
"explanation": "Identity + scopes for this token. `token.effective_scopes` is the intersection of the token's granted bits and the user's live permissions ...",
"next_step": { "method": "POST", "path": "/user_access/api/tools/search", "description": "..." }
}
Check effective_scopes before attempting a gated action rather than discovering the gate by hitting a 403. scopes_filtered_by_user_permissions lists grants the token carries but the user cannot exercise.
Step 3: The MCP portal tools and their HTTP equivalents
The hosted MCP server exposes nine tools. They are a portal, not the full surface: the pattern is search → read the tool guide → call the endpoint. Each row below is exactly what the MCP server does when the tool is invoked, so calling the HTTP endpoint directly gives identical behavior.
MCP tool | HTTP equivalent | Body / notes |
|
| Step 1 above. Token header only. |
|
| No body. Orientation packet: positioning, agent contract, workflow index. |
|
| Ordered recipe for one intent. |
|
| Full prose, paths, and required scopes for one tool group. |
|
|
|
|
|
|
| Any | The same request with the two headers. Nothing outside the namespace is reachable. |
|
|
|
|
|
|
All of these require only the baseline can_read_all_endpoints scope, except that qvasa_query_metric additionally requires the user to have dashboard authoring access, and qvasa_call_api inherits whatever scope the target endpoint declares.
qvasa_overview → GET /guidance/overview
Call this first in a fresh conversation. It returns a compact packet (under 8 KB) with positioning, an agent_contract of do's and don'ts, a when_to_use list, a workflows index of intent ids with one-line triggers, the mcp_tools list, and next_steps. Users with dashboard-scoped access receive a shorter packet with a capability_profile block instead.
curl https://www.qvasa.com/user_access/api/guidance/overview \ -H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION"
qvasa_get_workflow → GET /guidance/workflows/:intent_id
Fetches one recipe as an ordered list of steps. Intent ids currently include metric_count_or_rate, metric_by_dimension_ranked, trend_over_time, executive_summary, answer_needs_classifier, clean_up_reason_codes, ticket_drill_in, reverse_lookup_value, browse_catalog, fill_component_configurations, build_formula_widget, setup_ticket_field_sla, manage_in_flight, resolve_date_range, troubleshoot_error_or_no_data, capability_not_found, and submit_feedback. The overview's workflows array is the authoritative list. An unknown id returns 404 with the full menu in available.
GET /user_access/api/guidance/workflows/metric_count_or_rate{
"trigger": "user asks 'how many X' / 'what's the rate' / 'show me a count or breakdown'",
"steps": [
"qvasa_search_tools — search for 'saved widgets' or 'dashboard widgets' FIRST; ...",
"qvasa_search_tools — query 'widgets' / 'metrics' / the user's phrasing ...",
"qvasa_get_tool_guide(tool_name='widgets') — read the widget tool's playbook ...",
"qvasa_call_api — POST /user_access/api/tools/widgets/search with {query} to resolve a widget; GET /user_access/api/tools/widgets/<widget_name> ...; POST /user_access/api/filters/date_and_time/discover with {phrase} for the date filter.",
"qvasa_query_metric — kicks off the async metric ... and polls until terminal."
],
"intent_id": "metric_count_or_rate"
}
When a step says qvasa_call_api, make the named HTTP request directly. When it says qvasa_query_metric, follow the kickoff-and-poll procedure below.
qvasa_get_tool_guide → GET /guidance/tools/:tool_name
Returns the manifest entry for one tool group: name, method, path, a long description that doubles as the playbook, every paths entry in the group, and required_scopes. Tool group names are the name values in toolkit.tools from Step 1, for example widgets, filters, metric_requests, ticket_sets, dashboards, dashboard_widgets, qvasa_support_tickets, batch, runbooks. An unknown name returns 404 with the full list in available.
Note the distinction: /guidance/tools/:tool_name describes a tool group. Individual tool definitions with a body_shape and playbook (for example count_tickets or create_dynamic_dashboard) live at GET /user_access/api/tools/:name, which is where search results point.
qvasa_search_tools → POST /tools/search
The primary discovery verb. Pass the user's phrasing; the catalog is already filtered to what this token can call.
curl -X POST https://www.qvasa.com/user_access/api/tools/search \
-H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" \
-H "Content-Type: application/json" \
-d '{"query": "how many tickets were created"}'
{
"query": "how many tickets were created",
"tool_results": [
{
"name": "count_tickets",
"summary": "Get a single number — how many tickets match a given filter scope.",
"score": 25,
"match_reasons": [ "tag token overlap: \"how many tickets\" (2)", ... ],
"final_endpoint": { "method": "POST", "path": "/user_access/api/quick_stats" },
"next_step": { "method": "GET", "path": "/user_access/api/tools/count_tickets", "description": "Fetch the full playbook for count_tickets." }
},
...
],
"metric_routing": { ... },
"construction_paths": { ... },
"fallback_results": { ... },
"saved_widget_results": [ ... ],
"runbook_results": [ ... ],
"explanation": "Top tools by match score. Pick one and call its next_step ... IMPORTANT: this reads as an analytics/metric question — metrics are NOT tools. See metric_routing ..."
}
How to read the response:
tool_resultsare ranked tool definitions. Trust a hit withscoreof 75 or higher or a fittingfinal_endpoint; otherwiseGETitsnext_step.pathto read thebody_shapeand playbook.metric_routingappears when the query is really a metric question. Metrics are widgets, not tools: resolve one viaPOST /user_access/api/tools/widgets/searchwith{"query": "..."}, then run it with the metric procedure below.saved_widget_resultsare the user's pre-built saved queries. Prefer re-running one (POST /user_access/api/dashboard_widgets/:id/query) over building from scratch.runbook_resultsare pre-built multi-step recipes. Prefer a matching runbook over hand-assembling a sequence; fetch one withGET /user_access/api/runbooks/:name.fallback_resultslists widgets and filters that fuzzy-matched when no tool did.
For a last-resort browse, GET /user_access/api/tools returns the exhaustive catalog of tools you can call.
qvasa_query_metric → kickoff, then poll
Metric calculation is asynchronous. The MCP tool hides this behind a single call; over HTTP you do it in two steps.
1. Kick off. Choose the endpoint by chart type and POST the widget name (this is the metric_name) plus optional filters:
| Kickoff endpoint |
|
|
|
|
|
|
|
|
|
|
curl -X POST https://www.qvasa.com/user_access/api/quick_stats \
-H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" \
-H "Content-Type: application/json" \
-d '{"metric_name": "tickets_created"}'
HTTP 202 Accepted
{
"async_request_uuid": "aabc73b0-4751-49bf-94cd-fec757a39a10",
"metric_kind": "quick_stat",
"request_kind": "metric",
"status": "enqueued",
"enqueued_at": "2026-09-17T18:48:38.982Z",
"next_step": {
"method": "GET",
"path": "/user_access/api/metric_requests/aabc73b0-4751-49bf-94cd-fec757a39a10",
"first_poll_after_ms": 5000,
"retry_after_ms": 5000,
"description": "Poll until status is 'completed', 'errored', or 'cancelled'. ..."
},
"cancel_step": { "method": "POST", "path": "/user_access/api/metric_requests/aabc73b0-.../cancel", ... },
"summary_step": { "method": "GET", "path": "/user_access/api/metric_requests", ... },
"applied_defaults": { ... }
}
2. Poll GET /user_access/api/metric_requests/:uuid until status is completed, errored, or cancelled. The MCP server polls every 1.5 seconds for up to 12 seconds; the kickoff's next_step suggests 5-second intervals. Most quick stats finish in a few seconds.
{
"async_request_uuid": "aabc73b0-4751-49bf-94cd-fec757a39a10",
"metric_kind": "quick_stat",
"status": "completed",
"completed_at": "2026-09-17T18:48:42.468Z",
"result": {
"metric_name": "tickets_created",
"metric_type": "quick_stat",
"widget_name": "Tickets Created Last 7 Days",
"current_value": 268,
"display_value": 268,
"start_datetime": "2026-09-10T07:00:00.000+00:00",
"end_datetime": "2026-09-18T06:59:59.999+00:00",
"historical_value": 0,
"percentage_change": "1000 (max)",
"directional_change": "up",
"up_is_good": false,
"filters_applied": { "filters_date_and_time": "{\"time_unit\":\"days\",\"time_length\":\"7\"}" },
"applied_filters_summary": [ { "type": "include", "label": "Date & Time", "values": [ "Last 7 days" ] } ]
}
}
Rules that save round trips:
metric_namemust be a widget name from this account's library. Resolve it withPOST /user_access/api/tools/widgets/search({"query": "tickets created"}) and read the widget's detail atGET /user_access/api/tools/widgets/:widget_namefor itswidget_type, required filters, andcomponent_configurations. An unknown name returns404unknown_metricwith that search endpoint asnext_step.Omit
filters_jsonand the metric defaults to the last 7 days (reported inapplied_defaults).Never hand-build a date filter.
POST /user_access/api/filters/date_and_time/discoverwith{"phrase": "last 30 days"}returns a readyfilters_fragment; merge it verbatim intofilters_json. Other filters come fromPOST /user_access/api/tools/filters/searchand their ownfilters_fragmentresponses. Filter fragments are opaque: never edit their keys or values.Drill into the tickets behind a number with the inspect variants:
POST /quick_stats_inspect,/basic_charts_inspect,/donut_charts_inspecttake the same body and materialize a ticket set. The completed poll result carriesticket_set_idandticket_count; read it viaGET /user_access/api/ticket_sets/:id,/ticket_sets/:id/tickets_table, and/ticket_sets/:id/summaries.GET /user_access/api/metric_requestslists this session's in-flight requests;POST /metric_requests/:uuid/cancelcancels one that is stillenqueued.
qvasa_call_api → any request under /user_access/api/
The MCP's generic dispatcher simply forwards method, path, and body to the API with the two auth headers attached. Over HTTP, that is just the request itself. Allowed verbs are GET, POST, PUT, PATCH, DELETE; every path starts with /user_access/api/. Each endpoint enforces its own scopes, so the dispatcher grants no extra authority.
Conventions that apply to every endpoint you reach this way:
Write bodies are strict. Any key the endpoint does not declare is rejected with
422unknown_body_keys, listingunknown_keysandallowed_keys, and nothing is written. Read the tool'sbody_shapeatGET /user_access/api/tools/:namefirst.List endpoints are usually
POST .../indexwith an optionalfiltersobject and cursor pagination (pagination.next_cursor,has_more).Batch writes:
POST /user_access/api/batchruns up to 50 batch-safe operations in one call with{"mode": "all_or_nothing" | "best_effort", "operations": [{"method": "POST", "path": "/user_access/api/...", "body": {...}}, ...]}. Requirescan_write_all_endpoints.Every success envelope carries the resource, an
explanation, and usually anext_step. Every error envelope carrieserror,error_code, and oftennext_stepanddiagnosis.
curl -X POST https://www.qvasa.com/user_access/api/dynamic_dashboards \
-H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" \
-H "Content-Type: application/json" \
-d '{"name": "x", "bogus": 1}'HTTP 422
{
"error": "Unknown body key(s) [\"bogus\"]. UAF422K",
"error_code": "unknown_body_keys",
"unknown_keys": [ "bogus" ],
"allowed_keys": [ "name", "description", "dashboard_access_type" ],
"explanation": "Write bodies are strict: every key must be one this endpoint declares. Nothing was written. ...",
"next_step": { "method": "GET", "path": "/user_access/api/tools/create_dynamic_dashboard", "description": "Read this endpoint's body_shape, then resend with only declared keys." },
"diagnosis": { "summary": "...", "explain_to_user": "..." }
}
qvasa_submit_support_ticket and qvasa_list_support_tickets
The escape hatch is a two-way support loop with the Qvasa team. File a ticket for a missing tool, an unrecoverable error (echo any support_code from the error into context.support_code), or a capability request. Only can_read_all_endpoints is required.
POST /user_access/api/qvasa_support_tickets
{
"ticket_type": "bug_report",
"subject": "quick_stats returns 500 for widget X",
"body": "Steps to reproduce ...",
"context": { "support_code": "UAA500X" }
}
ticket_type is one of bug_report, unexpected_behavior, friction_point, feature_request, impossibility_report, question, other. The response also runs a tool search against your text, so if the capability actually exists you find out in the same turn. Then:
POST /user_access/api/qvasa_support_tickets/indexwith{"filters": {"unread_replies_only": true}}answers "did Qvasa get back to me?" in one call. Other filters:status,ticket_type,open_only,mine_only,search.GET /user_access/api/qvasa_support_tickets/:idreturns the full thread and marks the reply read.POST /user_access/api/qvasa_support_tickets/:id/messageswith{"body": "..."}replies, andPATCH /user_access/api/qvasa_support_tickets/:id/statuswith{"status": "open" | "closed"}reopens or closes.
Error envelopes
Every error is JSON with a human error string that ends in a support code (for example UAS401M) and a machine error_code. Read error_code and next_step, then act; the recovery hints are designed for in-conversation use.
HTTP |
| Meaning and recovery |
401 |
| No |
401 | (message | Token invalid, expired, or revoked. The user must generate a new one. |
401 |
| No |
401 |
| Session expired, revoked, or superseded by a newer session. POST |
401 |
| Toolkit was updated since this session started. POST |
403 |
|
|
404 |
|
|
404 |
| Wrong path or a tool this token cannot see. |
422 |
| Body did not match the contract. Compare with |
429 |
| More than 120 calls per minute, or more than 20 expensive calls per minute, in this session. Wait for |
500 |
| Carries a |
GET /user_access/api/guidance/errors/:error_code explains the domain-specific codes that can appear inside a completed metric's result (for example a dashboard widget's error banner); the transport-level codes above are documented in this table instead.
Alternative: call the MCP server directly with JSON-RPC
If you would rather speak MCP without an MCP client library, the hosted server at POST https://www.qvasa.com/mcp is a stateless Streamable HTTP endpoint that returns plain JSON. It accepts the same qva_uat_ token as a Bearer credential (OAuth is optional and only needed by clients that want the connect-by-URL flow). There is no session header to manage: the server creates or reuses the token's session internally on every request.
curl -X POST https://www.qvasa.com/mcp \
-H "Authorization: Bearer qva_uat_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
The result lists the nine tools with their inputSchema. Invoke one with tools/call:
curl -X POST https://www.qvasa.com/mcp \
-H "Authorization: Bearer qva_uat_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {
"name": "qvasa_query_metric",
"arguments": { "metric_kind": "quick_stat", "metric_name": "tickets_created" }
}
}'{
"jsonrpc": "2.0", "id": 2,
"result": {
"content": [ { "type": "text", "text": "{\"async_request_uuid\":\"...\",\"status\":\"completed\",\"result\":{\"metric_name\":\"tickets_created\",\"current_value\":268, ...}}" } ]
}
}
Tool results arrive as a JSON string inside
result.content[0].text; parse it. If the underlying HTTP call returned 4xx or 5xx, the response is flagged as an error and the text is the same error envelope described above.An
initializehandshake is accepted but not required; each request is independent.Server-Sent Events are not supported; keep the
Acceptheader as shown and expect a plain JSON body.An unauthenticated request returns
401with aWWW-Authenticateheader pointing at/.well-known/oauth-protected-resource; you can ignore that and send the Bearer token.qvasa_query_metricover this transport polls for you for up to 12 seconds; if the metric is still calculating it returnsstatus: "processing"with themetric_requests/:uuidpath to poll viaqvasa_call_api.
A complete minimal sequence
# 1. Session
SESSION=$(curl -s -X POST https://www.qvasa.com/user_access/api/sessions \
-H "Authorization: Bearer $UAT" | jq -r .session_token)
AUTH=(-H "Authorization: Bearer $UAT" -H "X-User-Access-Session: $SESSION" -H "Content-Type: application/json")# 2. Orientation and identity
curl -s https://www.qvasa.com/user_access/api/guidance/overview "${AUTH[@]}"
curl -s https://www.qvasa.com/user_access/api/me "${AUTH[@]}"# 3. Find the metric for the question
curl -s -X POST https://www.qvasa.com/user_access/api/tools/widgets/search "${AUTH[@]}" \
-d '{"query": "tickets created"}'# 4. Resolve the date range the user asked for
curl -s -X POST https://www.qvasa.com/user_access/api/filters/date_and_time/discover "${AUTH[@]}" \
-d '{"phrase": "last 30 days"}' # returns filters_fragment; merge it into filters_json# 5. Run it and poll
UUID=$(curl -s -X POST https://www.qvasa.com/user_access/api/quick_stats "${AUTH[@]}" \
-d '{"metric_name": "tickets_created", "filters_json": { ...filters_fragment... }}' | jq -r .async_request_uuid)
sleep 5
curl -s https://www.qvasa.com/user_access/api/metric_requests/$UUID "${AUTH[@]}"
Related articles
User Access Tokens and the Qvasa MCP: Connecting Claude to Your Account — enabling the feature, generating a token, and connecting Claude or other MCP clients.
Qvasa MCP Server — Security and Integration Reference — authentication, scopes, tenancy, and auditing for security reviewers.
