Integration basics
How the dashboard calls any tool on the MCP direct door. Every tool page builds on this.
The flow
- Log in to Audit 360. You already have the user's
auth_token, tenant base URL, subdomain and active role id. - Open a session once, when the user opens the Dashboard (details: How to use the session ID):
POST /api/sessionwith the four tenant headers. You get asessionId. The server starts loading the last 6 months of issues into that session in the background, so later tool calls are fast. - Call a tool:
POST /mcp-direct/with a JSON-RPCtools/callbody, the four tenant headers andX-A360-Session-Id. Noinitializecall is needed. - Read
result.structuredContent. Checksuccessfirst, then usedata.
Base URL
This is the dev server. It changes per environment. Every example on this site uses it.
Browser calls are allowed from Audit 360 dashboards (https://<tenant>.audit360.solutions, .com, .in) and from http://localhost or http://127.0.0.1 on any port (not https://localhost). The response header X-A360-Session-Id is exposed to browser JavaScript.
Headers
success: false and error_code: "token_expired" on /mcp-direct/ (HTTP 401 on the REST door). Send the user to log in again. A data call answered from the session's cache does not reach Audit 360, so it does not notice an expired token. See Errors for the other token cases.Step 2 · Open a session
Response (real, from the dev server):
- The id is in the body (
sessionId) and in the response headerX-A360-Session-Id. - Send it on every later call, always together with the four tenant headers.
- It expires after 20 minutes without use (
idleTimeoutSeconds: 1200). An expired, unknown or other user's id is not an error: the server quietly starts a new session and returns the new id inX-A360-Session-Id. Always keep the id from the latest response.
Step 3 · The JSON-RPC body
Every tool call has the same body. Only name and arguments change. Each tool page lists its arguments.
- Keep the trailing slash:
/mcp-direct/. Without it you get307. idis any number you choose. The answer carries the sameid.- A misspelled argument is ignored, not rejected. Check
params_usedin the answer: it echoes every non-empty argument the tool received, as sent, plus the defaults it filled in (top_n,stack_by). A misspelled name is missing from it.
Step 4 · Read the answer
result.structuredContentis the tool's answer as an object. Use it.result.content[0].textis the same answer as a JSON string (for AI clients). You do not need it.- A section that does not apply to the current filter or drill-down is
{"applicable": false}. Hide that block on screen. ""(empty string) means "not computable", for example a percentage change from zero, or an entity absent from one period. Show it as "–".- Widget tools:
data.comparisonMeta.isComputable: falsewithincomputableReasonmeans the call worked (success: true) but there is nothing to compare, e.g. no issues in this scope, or a clicked month that a period does not have. Show the reason instead of the widget. - Widget tools:
data.drillBreakdownis always present. Without a drill-down it is{"applicable": false}. Dropped issues are left out of every count.
Rules every widget tool follows
These hold for every widget tool (compare_*). Each tool page adds only what is special to it.
Dates
- Accepted formats:
YYYY-MM-DD,DD-MM-YYYYorDD-Mon-YYYY(e.g.2026-04-01,01-04-2026,01-Apr-2026). Both ends are included. - T1 is the baseline. It must not start after T2:
"T1 must be the earlier period: it starts after T2". The same start day and overlapping periods are accepted. - T1 and T2 together must fit in 2 years (Audit 360's limit), e.g.
"T1 and T2 together span 2024-04-01 to 2026-09-30; Audit 360 allows at most 2 years in one request, so from 2024-04-01 the latest end is 2026-04-01". - An issue belongs to a period by its created date.
Filters
- Names (
*_names) match exactly, and case matters:"north"is not"North". An unknown name gives an empty result, not an error. - Ids (
*_ids) come from the dashboard's filter list.3002and"3002"are the same."15,27"(same-named entries merged in a dropdown) counts as two ids. - A malformed id (
0,-1,"abc") is dropped silently. If every id of a field is malformed, the call is refused with an error that names the field; it is never run unfiltered.[]or leaving the field out means no filter. A well-formed id that does not exist gives an empty result and shows as"id 999999"incomparisonMeta.context.filters. - Ids and names on the same field must both match (AND). Sending
cluster_ids: [3001]withcluster_names: ["Maharastra"](a different cluster) gives 0 issues. - Leave a filter out, or send
[], for "all". Filters still apply under a drill-down.
Drill-down
- Every drill starts at one clicked item (the tool's first
drill_*argument). Every otherdrill_*argument needs it, or you get e.g."a drill-down starts at a zone point: give drill_zone". - The server is stateless about clicks: send the whole path on every call.
drill_viewcan be left out. The server then picks it from what you send: the across path →across,drill_geo_path→geography, elsetime.drill_geo_pathor the across path sent with a different, explicit view is an error, e.g."drill_geo_path belongs to the geography view".drill_time_pathis the exception (see below).- Time view opening level. It depends on Period 2's span, counted as end date minus start date in days: up to 35 → week, up to 100 → month, up to 365 → quarter, longer → year. Example: Jul 1–Sep 30 is 91 → month; Jul 1–Aug 5 is 35 → week. The first
drill_time_pathstep must be that level, and each next step one level finer (year → quarter → month → week). Otherwise:"time step 1 must be a month: Period 2's range opens at month level, and each click goes one level finer (year > quarter > month > week)". - A time step is
{"level": "year"|"quarter"|"month", "period_number": 1..60}, with no other keys. A week is never drilled into: its click opens the raw records. period_numbercounts from each period's own start: Month 1 is the first month of T1 and of T2. A number a period does not have (Month 4 of a 3-month period) is not an error:isComputablebecomesfalse.drill_time_pathalso narrows the geography view (and the across view, where the tool allows it): both periods shrink to the clicked bucket. Send it only while the screen is inside that bucket.drill_geo_pathlists names clicked in the order zone → region → state → cluster, at most 4 names. A 5th gives"an auditee is not drilled into: its click opens the raw records".- A clicked name must lie inside an active filter on the same level, or you get e.g.
"drill-down zone 'North' is outside the active zone filter". A clicked name with no issues is not an error: you get zeros orisComputable: false. drill_risk_levels(where the tool has it) narrows nothing. EverydrillBreakdown.items[]always hasriskSplit:[]without levels, else one{riskLevel, T1Count, T2Count, change}per picked level. A level sent twice is an error.drillBreakdownhasdescription,dimension(time/geography/across),level,windows(each period's real dates),totalItemsanditems[]withname,periodNumber,T1Label,T2Label(dates for time items,""otherwise),presence(both/T1_only/T2_only),T1Count,T2Count,change,changePct,direction,riskSplit.
Errors
There are four kinds. Check them in this order.
The REST door is different
Narrator prompts
Each widget tool has a narrator prompt that turns its JSON into a written summary. Get it with prompts/get on the same door, or call the tool get_all_prompts. Send the prompt text to the LLM as the system prompt and the tool's JSON as the user message.
Summary "i" button
Call get_all_summary_definitions once at app load, or when a comparison summary first opens. Send category: "comparison". Cache the answer for the session because definitions change only with a server release.
Each point has one definition string containing one sentence, up to 100 words. Period 1 is the dashboard's Comparison range; Period 2 is its Primary range.
JSON-RPC request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_all_summary_definitions",
"arguments": { "category": "comparison" }
}
}Match a summary point
- Take the widget whose
nameequals the summary's narrator prompt name. - Read the exact label from
- **<label>:**at the start of the summary point. - Find the point whose
labelscontains that label and whosemetricisanyor the current chart metric. - For Top/Bottom Auditees, use metric
issuesfor No Of Issues andcompliancefor Achieved Compliance %.
Show the "i" button only when a point matches. The Drill-Down point has no definition, so it has no "i" button.
Small JavaScript example
const data = response.result.structuredContent.data;
sessionStorage.setItem("summaryDefinitions", JSON.stringify(data));
function findDefinition(promptName, label, metric) {
const widget = data.widgets.find(item => item.name === promptName);
const point = widget?.points.find(point =>
point.labels.includes(label) &&
(point.metric === "any" || point.metric === metric)
);
return point?.definition;
}
const definition = findDefinition(promptName, label, metric);
infoButton.hidden = !definition;
infoText.textContent = definition ?? "";What to show
Show the matched point's definition string as one text entry in the information button. Do not split it into fields. Allow the text to wrap on a small screen.
For Status Mix in Schedule By Audit Type, the button shows:
Schedules by Audit Start Date are counted under their Execution Status, leaving out statuses outside the 16 documented values, and the 3 statuses with the most Period 2 schedules, out of all statuses, show Period 1 and Period 2 counts and the status's share of all Period 2 schedules with its change in percentage points.