Skip to content

Model Context Protocol

Connect Claude Code, Codex, Gemini CLI, Grok Build, Cursor or OpenCode to Strac Comply, or another MCP client that signs in from your own machine. Your agent reads your live compliance status and writes evidence back for you. Every write is logged with the agent that made it and the person who signed it in.

Quickstart

For Claude Code, add the server before you start a session, then authenticate once:

bash
claude mcp add --transport http --scope user strac-comply \  https://mcp.comply.strac.io/mcp
  1. Run the command above. --scope user makes the server available in every Claude Code session, not just the current folder.
  2. Start (or restart) Claude Code. MCP config is read at launch.
  3. Run /mcp → select strac-comply → Authenticate. Your browser opens to sign in and grant scopes. The token persists and refreshes.

For Codex, one command adds the server and opens your browser to sign in:

bash
codex mcp add strac-comply --url \  https://mcp.comply.strac.io/mcp

If Codex says the server may or may not require login, run codex mcp login strac-comply.

Gemini CLI, Grok Build, Cursor and OpenCode connect to the same server:

Gemini CLI

bash
gemini mcp add --scope user --transport http strac-comply \  https://mcp.comply.strac.io/mcp

Gemini opens your browser to sign in. If it does not, run /mcp auth strac-comply in a session.

Grok Build

bash
grok mcp add --transport http strac-comply \  https://mcp.comply.strac.io/mcp

Grok opens your browser to sign in the first time you use it.

Cursor

json~/.cursor/mcp.json
{  "mcpServers": {    "strac-comply": { "url": "https://mcp.comply.strac.io/mcp" }  }}

Save it, then sign in from Cursor Settings > MCP.

OpenCode

json~/.config/opencode/opencode.json
{  "mcp": {    "strac-comply": { "type": "remote", "url": "https://mcp.comply.strac.io/mcp" }  }}

OpenCode opens your browser to sign in the first time you use it.

Using another MCP client that signs in from your machine? Point it at https://mcp.comply.strac.io/mcp.

Authentication

The server uses OAuth 2.1 with PKCE (access tokens mcp_at_*, refresh mcp_rt_*). Spec-compliant clients run the discovery + PKCE flow for you. The full handshake (RFC 9728 / 8414 metadata, dynamic registration, callback rules) is in Authentication.

Scopes

tools/list is filtered to the scopes your token was granted, so your agent only sees tools it can actually call.

ScopeWhat it allows
compliance:read
Read controls, frameworks, audits, tests, and the audit log.
policies:read
Read policies and policy versions.
policies:write
Upload and update policies (draft state); set policy owners.
policies:approve
Approve a policy version (lifecycle write).
documents:read
Read documents and fetch presigned download URLs.
documents:write
Upload and publish document versions; set document owners.
tests:write
Re-run an automated test, import historic test-run evidence (GRC migration), propose test waivers, exclude resources from scope, assign check owners, and tighten (never loosen) the vulnerability reminder settings, including posting summaries to a Slack channel.
personnel:read
Read the employee directory.
audits:read
Read audit binders and evidence requests.
evidence:write
Attach evidence to controls; mark controls Not Applicable; set control owners; tighten completion-drop alerts.
chat:ask
Ask freeform compliance questions grounded on live posture.
vendors:read
Read the managed-vendor list.
vendors:write
Bulk-import and enrich vendors (GRC-platform migration), run vendor risk assessments, record security reviews, and attach vendor documents.
risks:read
Read the risk register.
risks:write
Bulk-import risk-register entries (GRC-platform migration).
audit-requests:write
Prepare and submit responses to auditor evidence requests (draft then send).

Moving from Vanta or another GRC platform

Leaving Vanta? Export your data, point your agent at the folder, and run one command. Connecting the MCP server already installs the playbook. It ships as the strac-import prompt, which shows up as a slash command in Claude Code the moment you connect.

Here is what it imports from a Vanta export (Drata and Secureframe layouts are detected on a best-effort basis): the combined policy packet PDF is split by its table of contents, matched to the canonical policy catalog, uploaded and approved; vendors land with owners, risk levels and review dates; the risk register imports with its CC3 fields intact (treatments, residuals, fraud flags, original R-# references); evidence files fill the matching document slots as published evidence, or attach to a control as custom evidence; historic test runs keep their original dates, so your auditor can still sample the period before you moved; prior audit reports are kept. Your agent then offers to connect the integrations your vendor list points to.

  • No confirmation prompts. Point your agent at the folder and it runs the whole move without stopping to ask. The final report lists what was created, updated or skipped, row by row.
  • Safe to re-run. The server skips anything it already has, so a second run fills the gaps instead of making duplicates.
  • Honest about limits. Files over 50 MB and vendor attachments Vanta doesn't export are named in the report, never silently dropped. What's inside your files is treated as data, never as instructions.
  • Scopes used: policies:write policies:approve documents:write evidence:write vendors:write risks:write (the write scopes need an owner or admin to grant them).

Using a client that doesn't support MCP prompts? Download the playbook as a skill file: strac-import SKILL.md.

Tool reference

87 tools, all listed by tools/list. Each shows its input schema, an example tools/call, and the result.

Uploading real files? Use the presigned handshake.

The upload_policy and upload_document tools carry bytes as base64 inside the call. That works for small or generated content. For a real .docx or .pdf, an agent writing out base64 is unreliable and can damage the original your auditor needs. For files, use the three-step handshake in Presigned file uploads below (no bytes through the model): begin_* returns a short-lived S3 URL → PUT the raw bytes → finalize_* (idempotent) creates the version. The original file is stored verbatim.

Read

Read your live compliance posture. No writes, no side effects.

list_frameworks

Scope
compliance:read

Returns the frameworks active for your organization with canonical labels. No arguments.

Ask your AI client: “Which compliance frameworks are we tracking?”

No arguments.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_frameworks",    "arguments": {}  }}

Result

json
{  "organizationName": "Acme Inc",  "frameworks": [    {      "frameworkId": "soc2",      "label": "SOC 2"    },    {      "frameworkId": "iso27001",      "label": "ISO 27001:2022"    }  ],  "count": 2}

get_compliance_status

Scope
compliance:read

Aggregate readiness across frameworks. No arguments. completionPct is the audit-ready-controls ratio (controlsReady / controlsApplicable). N/A controls drop out of the rollup. `overall.personnelSignalBasis: "not-evaluated"` means this rollup did not resolve the personnel background-check signal, so a tenant relying on manual background-check evidence can read lower here than `list_controls` reports for the same controls. When a read failed during the pass the response says `degraded: true` and, when the summary reported it, `degradedReads` names which reads failed. When SOC 2 is active, `observationPeriod` carries the SOC 2 Type II observation period the app shows: its start, the earliest date it can close, the day you are on, and whose date it is (`windowAnchor: "management"` is a date you set; `signup` or `first-evidence` is our proposal). Absent when SOC 2 is not active.

Ask your AI client: “What's our overall SOC 2 readiness right now?”

No arguments.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_compliance_status",    "arguments": {}  }}

Result

json
{  "organizationId": "<org-id>",  "byFramework": [    {      "frameworkId": "SOC2",      "label": "SOC 2",      "controlsTotal": 64,      "controlsApplicable": 62,      "controlsNotApplicable": 2,      "controlsReady": 40,      "completionPct": 65,      "percentageBasis": "audit-ready-controls"    }  ],  "overall": {    "completionPct": 64.5,    "percentageBasis": "audit-ready-controls",    "personnelSignalBasis": "not-evaluated",    "controlsTotal": 64,    "controlsApplicable": 62,    "controlsReady": 40  },  "observationPeriod": {    "frameworkId": "soc2",    "windowDays": 90,    "windowStartedAt": "2026-09-01T00:00:00.000Z",    "windowAnchor": "signup",    "earliestCloseAt": "2026-11-30T00:00:00.000Z",    "dayOfWindow": 37,    "notStarted": false,    "windowComplete": false,    "evidenceCollectionStartedAt": "2026-09-03T14:12:00.000Z",    "uncoveredLeadDays": 2  }}

list_controls

Scope
compliance:read

Cursor-paginated control list with completion % and readiness signal. `truncated: true` means more controls exist than this page holds (`total` is the full count); pass `nextCursor` back as `cursor` for the next page.

Ask your AI client: “List the SOC 2 controls that still have gaps.”

Input

FieldTypeDescription
frameworkIdoptionalstring, 1–80Filter to one framework.
limitoptionalinteger 1–200Page size.Default: 50
cursoroptionalstringOpaque pagination cursor from a prior nextCursor.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_controls",    "arguments": {      "frameworkId": "soc2",      "limit": 50    }  }}

Result

json
{  "controls": [    {      "controlId": "CC6.1",      "frameworkId": "soc2",      "frameworkReference": "CC6.1",      "name": "Logical access controls",      "category": "Logical & Physical Access",      "completionPct": 67,      "readinessSignal": "gap",      "notApplicable": false,      "notApplicableReason": null,      "requiredCounts": {        "satisfied": 2,        "total": 3      }    }  ],  "nextCursor": null,  "pageSize": 1,  "truncated": false,  "total": 1}

get_control

Scope
compliance:read

A single control with framework mappings, completion, and attached-evidence counts.

Ask your AI client: “Show me the details and evidence for control CC6.1.”

Input

FieldTypeDescription
controlIdrequiredstring, 1–120Control id, e.g. CC6.1.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_control",    "arguments": {      "controlId": "CC6.1"    }  }}

Result

json
{  "controlId": "CC6.1",  "name": "Logical access controls",  "description": "The entity implements logical access security controls over protected information assets to protect against threats from outside its system boundaries.",  "frameworkId": "soc2",  "frameworkReference": "CC6.1",  "category": "Logical & Physical Access",  "frameworkMappings": [    {      "frameworkId": "iso27001",      "controlId": "A.8.3",      "controlTitle": "Information access restriction"    }  ],  "completionPct": 67,  "readinessSignal": "gap",  "auditGaps": [    "No approved access-review policy within the review window"  ],  "notApplicable": false,  "notApplicableReason": null,  "markedNAAt": null,  "markedNABy": null,  "requiredCounts": {    "satisfied": 2,    "total": 3  },  "attached": {    "policiesCount": 1,    "documentsCount": 1,    "testsPassingOf": {      "passing": 4,      "total": 5    }  },  "lastTestDate": "2026-05-20T00:00:00.000Z"}

get_next_actions

Scope
compliance:read

The prioritized, deduplicated list of missing required evidence for a framework: which policy to approve, which document to upload, which test to fix, each with the controls it unblocks and whether it is a quick win (already uploaded, just approve). Clearing every step drives most controls to audit-ready; a residual below 100% with zero steps is valid (some controls track evidence beyond this checklist). `personnelSignalBasis: "not-evaluated"` means this pass did not resolve the personnel background-check signal, so a background-check step may appear that `list_controls` already counts as satisfied. A step carries `commitment { owner, expectedDate, overdue }` when someone has been named for it (the evidence commitment); an overdue commitment leads the list. When a read failed during the pass the response says `degraded: true` and, when the summary reported it, `degradedReads` names which reads failed.

Ask your AI client: “What do I need to do to get SOC 2 to 100%?”

Input

FieldTypeDescription
frameworkIdoptionalstringOptional framework to restrict to, e.g. SOC2. Omit for all active frameworks.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_next_actions",    "arguments": {      "frameworkId": "SOC2"    }  }}

Result

json
{  "organizationId": "org_example",  "personnelSignalBasis": "not-evaluated",  "frameworks": [    {      "frameworkId": "SOC2",      "label": "SOC 2",      "completionPct": 55,      "totalRemaining": 14,      "quickWinCount": 5,      "actions": [        {          "id": "POL-SEC-005",          "title": "Vulnerability Management Policy",          "type": "policy",          "status": "draft",          "quickWin": true,          "unblocksControls": [            "CC6.1",            "CC7.1",            "CC7.2"          ],          "unblocksCount": 3,          "deepLink": "/compliance/policies/POL-SEC-005"        },        {          "id": "test-s3-encryption",          "title": "S3 bucket encryption",          "type": "test",          "status": "failing",          "quickWin": false,          "unblocksControls": [            "CC6.1"          ],          "unblocksCount": 1,          "deepLink": "/tests/test-s3-encryption",          "commitment": {            "owner": "jane@example.com",            "expectedDate": "2026-09-15",            "overdue": false          }        }      ]    }  ]}

list_policies

Scope
policies:read

Policies with lifecycle status and review dates. When a policy's newest version is a draft waiting for approval, the row carries `draft` with its versionId and `approvable`: false (with the reason and what fixes it) when approve_policy would refuse it, exactly as the Policies page shows. Top-level counts say how many drafts are waiting, approvable and blocked.

Ask your AI client: “What policies do we have, which are overdue for review, and which drafts can I approve now?”

Input

FieldTypeDescription
frameworkIdoptionalstring, 1–80Filter by framework.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_policies",    "arguments": {}  }}

Result

json
{  "policies": [    {      "policyId": "POL-SEC-001",      "name": "Information Security Policy",      "frameworks": [        "SOC 2"      ],      "lifecycleStatus": "approved",      "approvedAt": "2026-02-27T00:00:00.000Z",      "nextReviewDate": "2027-02-27T00:00:00.000Z",      "isOverdue": false    },    {      "policyId": "POL-SEC-002",      "name": "Access Control Policy",      "frameworks": [        "SOC 2"      ],      "lifecycleStatus": "draft",      "approvedAt": null,      "nextReviewDate": null,      "isOverdue": false,      "draft": {        "versionId": "<version-id>",        "version": 1,        "aiGenerated": true,        "approvable": false,        "approvalBlocked": "unresolved_placeholders",        "approvalBlockedReason": "This draft still has sections marked [REVIEW] … Complete those answers and regenerate, or edit the policy and fill them in yourself, before approving it."      }    }  ],  "count": 2,  "draftsAwaitingApproval": 1,  "draftsApprovable": 0,  "draftsBlocked": 1}

get_policy

Scope
policies:read

A policy with control mappings and its latest version metadata. Accepts POL-* and CUSTOM-POL-* ids.

Ask your AI client: “Show the version history for POL-SEC-001.”

Input

FieldTypeDescription
policyIdrequiredstring, 1–120Policy id, e.g. POL-SEC-001.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_policy",    "arguments": {      "policyId": "POL-SEC-001"    }  }}

Result

json
{  "policyId": "POL-SEC-001",  "name": "Information Security Policy",  "framework": [    "SOC 2"  ],  "isCustom": false,  "lifecycleStatus": "approved",  "approvedAt": "2026-02-27T00:00:00.000Z",  "approvedBy": "ceo@example.com",  "isOverdue": false,  "latestVersion": {    "versionId": "<version-uuid>",    "versionNumber": 3,    "uploadedAt": "2026-02-20T00:00:00.000Z",    "uploadedBy": "ciso@example.com",    "format": "pdf"  }}

list_documents

Scope
documents:read

Catalog document slots for your ACTIVE frameworks (N/A slots included, with their reason) plus ad-hoc uploads, with lifecycle status. `frameworkScope` says whether the catalog was scoped to your active frameworks. A slot with `supportingOnly: true` is not required by those frameworks (an empty one is not a gap); `alternativeFor` names the required documents it can stand in for.

Ask your AI client: “List our catalog documents and their publish status.”

Input

FieldTypeDescription
sourceoptionalstringWhich documents to include.catalogadhocallDefault: all
frameworkIdoptionalstring, 1–80Filter catalog docs by framework.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_documents",    "arguments": {      "source": "all"    }  }}

Result

json
{  "documents": [    {      "documentId": "DOC-CC1-ORG-CHART",      "title": "Company Organization Chart",      "source": "catalog",      "framework": [        "SOC 2"      ],      "lifecycleStatus": "published",      "publishedAt": "2026-05-01T00:00:00.000Z",      "nextReviewDate": "2026-08-01T00:00:00.000Z",      "isOverdue": false    }  ],  "count": 1,  "frameworkScope": "active"}

list_employees

Scope
personnel:read

The same roster as the dashboard's People page: Google Workspace, every connected identity provider and your HRIS, one row per person, with terminations from the HRIS, admin overrides and completed offboarding checklists. `sources` names a directory that could not be read in full. No sensitive PII.

Ask your AI client: “Who's on the active engineering roster?”

Input

FieldTypeDescription
statusoptionalstringEmployment status filter (terminated also returns suspended accounts).activeterminatedallDefault: active
departmentoptionalstring, 1–120Case-sensitive department match.
includeServiceAccountsoptionalbooleanInclude accounts marked as service accounts.Default: true

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_employees",    "arguments": {      "status": "active",      "department": "Engineering"    }  }}

Result

json
{  "employees": [    {      "email": "alice@example.com",      "name": "Alice Smith",      "jobTitle": "Security Engineer",      "department": "Engineering",      "status": "active",      "source": "okta",      "hireDate": "2024-01-15",      "terminationDate": null,      "isServiceAccount": false,      "mfa": "enabled"    }  ],  "count": 1,  "terminationSignal": "resolved",  "sources": [    {      "source": "okta",      "label": "Okta",      "count": 1,      "failed": false,      "truncated": false    }  ]}

list_lifecycle_checklists

Scope
personnel:read
+
compliance:read

Onboarding/offboarding lifecycle checklists (SOC 2 CC6.2/CC6.3 evidence of provisioning and access removal) with per-checklist progress and unresolvedRevocations (offboarding items whose access is not yet confirmed removed). Newest first; also requires compliance:read.

Ask your AI client: “Which offboardings still have access that hasn't been revoked?”

Input

FieldTypeDescription
typeoptionalstringChecklist-type filter.onboardingoffboardingallDefault: all
statusoptionalstringStatus filter.activecompletedvoidedallDefault: all

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_lifecycle_checklists",    "arguments": {      "type": "offboarding",      "status": "active"    }  }}

Result

json
{  "checklists": [    {      "episodeId": "ep_demo1",      "type": "offboarding",      "personEmail": "bob@example.com",      "personName": "Bob Jones",      "status": "active",      "terminationDate": "2026-06-30",      "progressDone": 4,      "progressTotal": 6,      "unresolvedRevocations": 2,      "createdBy": "admin@example.com",      "createdAt": "2026-06-30T09:00:00.000Z"    }  ],  "count": 1}

get_lifecycle_checklist

Scope
personnel:read
+
compliance:read

One onboarding/offboarding checklist with every item: verification status + source, manual confirmations (who/when + note), manual attestations on auto items, and linkedControls. For a completed checklist the item states are the frozen audit evidence; for an active one, the current server-verified state. Also requires compliance:read.

Ask your AI client: “Show me the full offboarding checklist for Bob, item by item.”

Input

FieldTypeDescription
checklistIdrequiredstring, 1–200The episodeId from list_lifecycle_checklists.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_lifecycle_checklist",    "arguments": {      "checklistId": "ep_demo1"    }  }}

Result

json
{  "checklist": {    "episodeId": "ep_demo1",    "type": "offboarding",    "personEmail": "bob@example.com",    "personName": "Bob Jones",    "status": "completed",    "terminationDate": "2026-06-30",    "createdBy": "admin@example.com",    "createdAt": "2026-06-30T09:00:00.000Z",    "completedBy": "admin@example.com",    "completedAt": "2026-07-02T17:00:00.000Z"  },  "items": [    {      "itemId": "gws-suspend",      "title": "Suspend Google Workspace account",      "kind": "auto",      "integration": "google-workspace",      "required": true,      "linkedControls": [        "CC6.3"      ],      "verification": {        "status": "satisfied",        "detail": "Account suspended in Google Workspace",        "source": "google-workspace",        "checkedAt": "2026-07-02T17:00:00.000Z"      },      "frozen": true    }  ]}

list_inventory

Scope
compliance:read

The categorized asset population an audit scope is drawn on (PCI DSS 12.5.1), the same merged snapshot the dashboard and auditor portal show. Org-wide counts (in-scope, scoped-out, manual, per-category) are always exact even when the row list is filtered or truncated by limit.

Ask your AI client: “What data stores are in scope for our audit?”

Input

FieldTypeDescription
categoryoptionalstringFilter returned rows to one category; org-wide counts are unaffected.computedata-storescode-registriesnetwork-edgesecurity-infrasaas-applicationsdevicesother
includeScopedOutoptionalbooleanInclude resources the admin marked out of scope (with their reason).Default: false
limitoptionalinteger 1–500Max rows returned.Default: 100

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_inventory",    "arguments": {      "category": "data-stores",      "limit": 100    }  }}

Result

json
{  "collectedAt": "2026-07-01T06:00:00.000Z",  "syncStatus": "idle",  "snapshotTruncated": false,  "coverage": {    "integrations": 2,    "accountIds": [      "111122223333"    ]  },  "counts": {    "inScope": 42,    "scopedOut": 3,    "manual": 2,    "byCategory": {      "compute": 12,      "data-stores": 8,      "code-registries": 4,      "network-edge": 5,      "security-infra": 3,      "saas-applications": 7,      "devices": 2,      "other": 1    }  },  "resources": [    {      "key": "aws:s3:acme-data",      "name": "acme-data",      "identifier": "arn:aws:s3:::acme-data",      "type": "S3 bucket",      "category": "data-stores",      "status": "active",      "firstSeen": "2026-05-01T00:00:00.000Z",      "source": {        "integrationType": "aws",        "integrationId": "int_demo1",        "accountId": "111122223333",        "region": "us-east-1"      },      "inScope": true,      "manual": false,      "owner": "alice@example.com",      "sensitivity": "confidential"    }  ],  "returned": 1,  "totalMatching": 8,  "resultsTruncated": false}

list_saas_accounts

Scope
personnel:read
+
compliance:read

The same inventory as the dashboard's SaaS Security › Accounts page: every account in Google Workspace, Microsoft Entra ID, Slack, GitHub, AWS IAM, Okta and your connected SaaS apps, with status, admin flag, MFA, last sign-in, owner, and whether it is a service account (marked by an admin, reported by the system, or detected by Strac and not yet reviewed). `unreadableSystems` names a connected system that could not be read; `noOwner` is null when ownership could not be measured. Read live. Also requires compliance:read.

Ask your AI client: “Which active accounts still don't have MFA enrolled?”

Input

FieldTypeDescription
statusoptionalstringAccount status filter.activeinvitedsuspendeddeactivatedallDefault: all
mfaEnrolledoptionalbooleanFilter by MFA enrollment; accounts whose system does not report MFA are excluded by either value.
systemoptionalstring, 1–80One system by name, case-insensitive substring.
noOwneroptionalbooleanOnly accounts nobody owns (ignored when ownership could not be measured).
serviceAccountoptionalbooleanOnly service accounts (true) or only the rest (false).
emailoptionalstring, 1–254Only accounts for this address.
limitoptionalinteger 1–500Cap results at N.Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_saas_accounts",    "arguments": {      "status": "active",      "mfaEnrolled": false    }  }}

Result

json
{  "count": 1,  "total": 42,  "truncated": false,  "connectedSystems": [    "Google Workspace",    "Slack",    "GitHub"  ],  "unreadableSystems": [],  "partialSystems": [],  "ownershipMeasured": true,  "summary": {    "total": 42,    "active": 38,    "admins": 4,    "withoutMfa": 1,    "mfaNotReported": 9,    "serviceAccounts": 3,    "detectedUnreviewed": 1,    "noOwner": 2  },  "accounts": [    {      "email": "bot@example.com",      "name": "Deploy Bot",      "system": "GitHub",      "role": "member",      "status": "Active",      "isAdmin": false,      "mfaEnrolled": false,      "lastLoginAt": null,      "neverSignedIn": false,      "createdAt": "2024-02-01T00:00:00.000Z",      "owner": {        "kind": "person",        "name": "Alice Smith",        "email": "alice@example.com"      },      "noOwner": false,      "serviceAccount": {        "kind": "marked",        "by": "alice@example.com",        "at": "2026-06-30T00:00:00.000Z",        "justification": "deploy bot"      }    }  ]}

list_saas_apps

Scope
compliance:read

Persisted shadow-IT discovery records (currently Google Workspace-centric): third-party OAuth apps with their honest first-seen timestamp, the admin triage decision + rationale (SOC 2 CC9.2 evidence), and per-app inventory (riskLevel, scopesCount, userCount, lastScannedAt; null means not yet scanned, never a fabricated zero). Reads stored rows, not a live scan, so coverage can lag the Shadow IT page; newest first.

Ask your AI client: “What shadow-IT OAuth apps still need review?”

Input

FieldTypeDescription
stateoptionalstringTriage-state filter.needs_reviewignoredrejectedallDefault: all
limitoptionalinteger 1–500Cap results at N.Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_saas_apps",    "arguments": {      "state": "needs_review"    }  }}

Result

json
{  "count": 1,  "total": 12,  "truncated": false,  "source": "persisted-discovery-records (not a live scan; Google Workspace-centric)",  "apps": [    {      "clientId": "oauth-client-demo1",      "appName": "Grammarly",      "state": "needs_review",      "firstSeenAt": "2026-06-01T00:00:00.000Z",      "stateChangedAt": null,      "stateChangedBy": null,      "decisionRationale": null,      "promotedToVendorId": null,      "riskLevel": "High",      "scopesCount": 4,      "userCount": 9,      "lastScannedAt": "2026-07-10T00:00:00.000Z"    }  ]}

list_audits

Scope
audits:read

Audits with lifecycle status and pending-request counts.

Ask your AI client: “What audits are in flight and how many requests are pending?”

Input

FieldTypeDescription
statusoptionalstringLifecycle filter.planningin-flightcompleteallDefault: all

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_audits",    "arguments": {      "status": "in-flight"    }  }}

Result

json
{  "audits": [    {      "auditId": "<audit-uuid>",      "title": "SOC 2 Type 2 (2026)",      "framework": "SOC 2",      "periodStart": "2026-01-01T00:00:00.000Z",      "status": "in-flight",      "pendingRequestsCount": 5,      "createdAt": "2025-11-01T00:00:00.000Z"    }  ],  "count": 1}

get_audit

Scope
audits:read

A single audit with sampled-control count, evidence-request breakdown, and findings rollup.

Ask your AI client: “Summarize the 2026 SOC 2 audit binder.”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id (UUID).

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_audit",    "arguments": {      "auditId": "<audit-uuid>"    }  }}

Result

json
{  "auditId": "<audit-uuid>",  "title": "SOC 2 Type 2 (2026)",  "framework": "SOC 2",  "status": "in-flight",  "sampledControlsCount": 30,  "evidenceRequests": {    "requested": 5,    "submitted": 3,    "accepted": 120,    "rejected": 2,    "request-more": 0,    "total": 130  },  "findings": {    "critical": 0,    "high": 1,    "medium": 2,    "low": 1,    "total": 4,    "open": 2  }}

get_audit_verdicts

Scope
audits:read

The auditor's per-control decisions with the comment left on each. Separate from get_audit's evidence-request counts. `verdicts[].status` is one of accepted | exception | needs-info | na | not-reviewed (the human-readable `decision` label renders accepted as "approved"). Items needing action sort first; `summary` counts every recorded verdict including not-reviewed, so summary.total can exceed verdicts.length unless includeNotReviewed is set.

Ask your AI client: “Which controls did the auditor flag, and what did they say?”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120The auditId from list_audits.
includeNotReviewedoptionalbooleanInclude controls the auditor has not yet reviewed.Default: false

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_audit_verdicts",    "arguments": {      "auditId": "<audit-uuid>"    }  }}

Result

json
{  "auditId": "<audit-uuid>",  "summary": {    "approved": 27,    "exception": 1,    "needsInfo": 1,    "na": 1,    "notReviewed": 5,    "total": 35  },  "followUpCount": 2,  "verdicts": [    {      "controlId": "CC6.1",      "status": "exception",      "decision": "exception raised: changes requested",      "comment": "Access review missing Q2 sample.",      "reviewedBy": "Jane Auditor",      "reviewedAt": "2026-06-10T14:00:00.000Z",      "auditorEmail": "auditor@example.com"    }  ]}

list_tests

Scope
compliance:read

Automated evidence-collection results, filterable by status/severity/integration. Rows carry compact substantive evidence when the test emitted it: population (true total/passing/failing counts) and passBasis (the threshold the verdict applied). Each row also carries the owner the Tests page Owner column shows: `owner` + `ownerSource` (assigned, with `ownerBasis` test or integration; connected; company-owner; unassigned). `ownerUnavailable: true` means an owner read failed, and those rows carry no owner fields. Use get_test_result for the full evidence drill-down.

Ask your AI client: “Show the failing high-severity AWS tests.”

Input

FieldTypeDescription
statusoptionalstringResult status filter.passingfailingskippederrorallDefault: all
severityoptionalstringSeverity filter.criticalhighmediumlowinfoallDefault: all
integrationIdoptionalstring, 1–120Filter to one integration, e.g. aws-123456789012.
limitoptionalinteger 1–500Max results.Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_tests",    "arguments": {      "status": "failing",      "severity": "high"    }  }}

Result

json
{  "count": 1,  "total": 1,  "truncated": false,  "tests": [    {      "testResultId": "<result-uuid>",      "testId": "aws-s3-encryption",      "testName": "S3 Encryption Check",      "status": "failing",      "severity": "high",      "category": "Security",      "integrationId": "aws-123456789012",      "message": "1 of 12 buckets does not have default encryption enabled.",      "executedAt": "2026-05-27T14:30:00.000Z",      "population": {        "total": 12,        "passing": 11,        "failing": 1,        "unit": "buckets"      },      "passBasis": "Passes when every bucket has default encryption enabled.",      "owner": "cloud@example.com",      "ownerSource": "assigned",      "ownerBasis": "integration"    }  ]}

get_test_result

Scope
compliance:read

One test result: the executor's message, when it ran, the status after accepted risk (matching list_tests), the failing resources (capped at 50, with the true total; `findingsReadFailed` when they could not be read, never an empty list), any whole-check waiver with who accepted it, the owner of the check as the Tests page shows it (`owner`, `ownerSource`, `ownerBasis`; `ownerUnavailable` when the owner read failed), and the full substantive evidence the test emitted: population counts, observedConfiguration (config values actually read), passBasis, passingResources (the compliant population, with truncation disclosed), collectionContext (source-account attestation), and consoleScreenshot (async console capture metadata). Screenshot download URLs are omitted by default. Pass includeScreenshotDownloadUrl:true only when you actually need to fetch the image (the presigned URL is a bearer-less link that lands in the conversation).

Ask your AI client: “Why did the S3 encryption test fail and how do I fix it?”

Input

FieldTypeDescription
testResultIdrequiredstring, 1–200Test result id.
includeScreenshotDownloadUrloptionalbooleanMint a short-lived (5 min) presigned download link for a ready console screenshot.Default: false

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_test_result",    "arguments": {      "testResultId": "<result-uuid>"    }  }}

Result

json
{  "testResultId": "<result-uuid>",  "testId": "aws-s3-encryption",  "testName": "S3 Encryption Check",  "status": "failing",  "severity": "high",  "message": "1 of 12 buckets does not have default encryption enabled.",  "executedAt": "2026-05-27T14:30:00.000Z",  "failingResources": [    {      "resourceId": "acme-audit-logs",      "title": "S3 bucket 'acme-audit-logs' does not have default encryption enabled."    }  ],  "failingResourcesTotal": 1,  "population": {    "total": 12,    "passing": 11,    "failing": 1,    "unit": "buckets"  },  "passBasis": "Passes when every bucket has default encryption (SSE-S3 or SSE-KMS) enabled.",  "collectionContext": {    "source": "aws",    "accountId": "123456789012",    "callerArn": "arn:aws:iam::123456789012:role/StracComplyAudit"  }}

list_waivers

Scope
risks:read

The waiver register, the same list as Risk › Waivers: accepted findings and whole-check waivers, accepted vulnerabilities, N/A controls, policies, documents and evidence items, PCI compensating controls, HIPAA determinations, scoped-out devices and resources, screening exemptions and paused checks, plus proposals awaiting a decision. Each entry carries its class (an alternative control is not an accepted risk), the reason verbatim, who recorded it, its end date and whether that date is enforced. `incomplete` names sources that could not be read. Owner or admin token.

Ask your AI client: “Which waivers have no end date?”

Input

FieldTypeDescription
classoptionalstringOnly this class.risk-acceptedscope-excludedalternative-controlmonitoring-pausedpending-decision
statusoptionalstringOnly this status ('expiring' = ends within 30 days).activeexpiringexpiredpendingrevoked
kindoptionalstringOnly waivers recorded in this place in the product (e.g. control-not-applicable).
noEndDateoptionalbooleanOnly in-force waivers with no end date.
limitoptionalinteger 1–500Cap results at N.Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_waivers",    "arguments": {      "noEndDate": true    }  }}

Result

json
{  "count": 1,  "total": 12,  "incomplete": false,  "summary": {    "inForce": 11,    "noEndDate": 3  },  "entries": [    {      "class": "risk-accepted",      "classLabel": "Risk accepted",      "kind": "test-finding-acceptance",      "subject": "aws-s3-encryption: legacy-logs bucket",      "reason": "Archive bucket, no customer data, scheduled for deletion.",      "recordedBy": "alice@example.com",      "recordedByKind": "person",      "recordedAt": "2026-05-01T00:00:00.000Z",      "endsAt": null,      "expiryBasis": "none",      "status": "active",      "link": {        "kind": "test",        "id": "aws-s3-encryption"      }    }  ]}

list_vulnerabilities

Scope
compliance:read

The same rows as the Vulnerabilities page: GitHub (from the last stored scan; when there is none, MCP asks for a background scan that usually lands within about 5 minutes and says so with `githubScanRequested`), AWS Inspector, Security Hub, Patch, CrowdStrike, Supabase and Jamf. Every count is a count of listed rows; each source reports `notListed` (findings a scanner counted but could not list) separately, never folded into a total. Sources that could not be read or are stale are named in `notes`.

Ask your AI client: “Which critical vulnerabilities are past their deadline?”

Input

FieldTypeDescription
sourceoptionalstringOne source ('github' = Dependabot + code scanning + secret scanning).dependabotcode-scanningsecret-scanningaws-inspectoraws-security-hubaws-ssmcrowdstrikesupabasejamfgithub
severityoptionalstringOnly this severity.criticalhighmediumlow
knownExploitedoptionalbooleanOnly findings on the CISA known-exploited list.
pastSlaoptionalbooleanOnly findings past their remediation deadline.
limitoptionalinteger 1–500Cap results at N.Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_vulnerabilities",    "arguments": {      "severity": "critical",      "pastSla": true    }  }}

Result

json
{  "count": 1,  "sources": [    {      "source": "aws-inspector",      "listed": 150,      "notListed": 12,      "notListedDisplay": "12",      "scannedAt": "2026-10-06T03:00:00.000Z",      "stale": false,      "readFailed": false    }  ],  "vulnerabilities": [    {      "source": "aws-inspector",      "severity": "critical",      "title": "CVE-2026-1234 in openssl",      "resource": "ecr:api-service",      "cve": "CVE-2026-1234",      "knownExploited": true,      "pastSla": true    }  ]}

list_access_reviews

Scope
personnel:read
+
compliance:read

The same campaigns as SaaS Security › Access reviews: state, period, frozen population, reviewers' decisions (kept / revoked / pending) and whether the access-request check ran. Pass `reviewId` for that review's frozen lines. Owner, admin or member token; also requires compliance:read.

Ask your AI client: “What did our last access review revoke?”

Input

FieldTypeDescription
reviewIdoptionalstring, 1–80A review's id; returns that review with its frozen lines (up to 500 accounts and 500 grants).

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_access_reviews",    "arguments": {}  }}

Result

json
{  "reviews": [    {      "reviewId": "<review-id>",      "name": "Q3 2026 access review",      "state": "closed",      "periodStart": "2026-07-01",      "periodEnd": "2026-09-30",      "accounts": {        "total": 42,        "kept": 39,        "revoked": 3,        "pending": 0      },      "authorizationCheck": {        "basis": "resolved",        "authorized": 30,        "joinable": 38      }    }  ],  "count": 1}

list_access_requests

Scope
personnel:read
+
compliance:read

The same list as SaaS Security › Access requests: who the access is for, the system, access level, status, and who authorized it and how (`signed-in`, `emailed-link`, or `as-stated`, which Strac did not verify). `recorded` rows are grants an administrator recorded after the fact, with their reason. `expiresAt` is recorded, never enforced. Approval links and tokens are never returned. Owner, admin or member token.

Ask your AI client: “Which access requests are still waiting for a decision?”

Input

FieldTypeDescription
statusoptionalstringFilter by status.pendingapproveddeniedprovisionedrecordedallDefault: all
limitoptionalinteger 1–500Cap results at N (newest first).Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_access_requests",    "arguments": {      "status": "pending"    }  }}

Result

json
{  "requests": [    {      "person": "bob@example.com",      "system": "GitHub",      "accessLevel": "admin",      "status": "pending",      "requestedAt": "2026-10-01T09:00:00.000Z",      "authorization": null,      "expiresAt": null    }  ],  "count": 1}

list_scope_exclusions

Scope
compliance:read

Lists every resource excluded from the checks that read it, across integrations (including Kandji and Jamf devices): the provider id, the label, the reason and category, who excluded it ("Strac support" for a support session, "Excluded automatically" for a device Strac detected as retired) and when. Oldest first. A failed read is an error, never an empty list.

Ask your AI client: “What have we excluded from scope, and which exclusions are oldest?”

Input

FieldTypeDescription
integrationTypeoptionalstringOnly this integration.
resourceKindoptionalstringOnly this kind.
limitoptionalinteger, 1–500Cap results.Default: 200

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_scope_exclusions",    "arguments": {      "integrationType": "aws"    }  }}

Result

json
{  "count": 1,  "total": 1,  "truncated": false,  "byIntegration": {    "aws": 1  },  "exclusions": [    {      "integrationType": "aws",      "integration": "AWS",      "resourceKind": "s3-bucket",      "kindLabel": "S3 bucket",      "resourceId": "acme-legacy-exports",      "resourceName": null,      "reason": "Bucket is being deleted this week; it holds no customer data.",      "reasonCategory": "decommissioned",      "excludedBy": "admin@example.com",      "excludedAt": "2026-10-06T15:00:00.000Z",      "excludedAtBasis": "excluded",      "automatic": false    }  ]}

get_vulnerability_reminder_settings

Scope
compliance:read

Reads Settings > Notifications > vulnerability deadline alerts: on or off, who gets the reminder besides each check's owner, which severities, the heads-up window, how often a still-overdue finding is reminded, extra addresses (and which are no longer members), the Slack channel, and who last changed them. `checkOwners` says who the reminder emails for each check: active members only, with `ownersNotMembers` (skipped) and `ownersEmailOff` (in-app only). Owners and admins may add `includeMembers` (the active members `alsoNotify` accepts) and `includeSlackChannels` (the channel ids `slackChannelId` accepts). Change the settings with set_vulnerability_reminder_settings, which can only tighten them. An unreadable settings row is an error, never the defaults.

Ask your AI client: “Who gets the vulnerability deadline reminders, and how often?”

Input

FieldTypeDescription
includeMembersoptionalbooleanOwner/admin: also list active workspace members (the addresses alsoNotify accepts).Default: false
includeSlackChannelsoptionalbooleanOwner/admin: also list the Slack channels a summary can be posted to (ids for slackChannelId).Default: false

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_vulnerability_reminder_settings",    "arguments": {}  }}

Result

json
{  "organizationId": "comp_demo",  "settings": {    "enabled": true,    "recipients": "admins-and-owners",    "severity": "critical-and-high",    "dueSoonDays": 7,    "repeat": "weekly",    "alsoNotify": [      "security@example.com"    ],    "alsoNotifyNotMembers": [],    "customized": true,    "updatedBy": "admin@example.com",    "updatedAt": "2026-10-14T00:00:00.000Z"  },  "checkOwners": {    "owners": {      "aws-vuln-critical": [        "cloud@example.com"      ]    },    "shared": [],    "readable": true,    "membersChecked": true  }}

get_completion_alert_settings

Scope
compliance:read

Reads when owners and admins are emailed that a framework's readiness went down (Settings > Notifications): `belowPercent` (alert when a framework crosses below it; 0 = off) and `dropPoints` (alert when it falls at least this far), plus `customized` and `updatedBy`/`updatedAt` of the last save. Pass `updatedAt` to set_completion_alert_settings.

Ask your AI client: “When does Strac email us that our SOC 2 readiness dropped?”

No arguments.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_completion_alert_settings",    "arguments": {}  }}

Result

json
{  "organizationId": "comp_demo",  "settings": {    "belowPercent": 80,    "dropPoints": 5,    "customized": true,    "updatedBy": "admin@example.com",    "updatedAt": "2026-10-12T15:00:00.000Z"  }}

Automated tests (write)

Act on automated evidence-collection tests: re-run one after a fix (and poll it to completion), attach the reference file an auditor reviews alongside a result, or propose an accept-risk waiver, which a DIFFERENT human admin must approve in the dashboard before it applies. None of these make a failing test pass.

rerun_test

Scope
tests:write

Re-runs ONE automated integration check; the run writes a NEW result row (find it with list_tests({ testId })). It does NOT make a failing test pass or change any evidence. A check failing on a real misconfiguration fails again. Owner/admin only. Returns immediately with status 'pending'; poll get_test_execution(executionId) for the outcome. Pass testId (from list_tests / get_next_actions) or testResultId.

Ask your AI client: “We enabled bucket encryption. Re-run the S3 encryption test.”

Input

FieldTypeDescription
testIdoptionalstring, 1–200Test-definition id to re-run (e.g. 'gws-mfa-enabled'). One of testId / testResultId is required.
testResultIdoptionalstring, 1–200Alternatively, the result-row id (from list_tests / get_test_result); its testId is resolved server-side.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "rerun_test",    "arguments": {      "testId": "aws-s3-encryption"    }  }}

Result

json
{  "executionId": "<execution-id>",  "testId": "aws-s3-encryption",  "integrationType": "aws",  "status": "pending",  "message": "Re-run enqueued for aws-s3-encryption. Poll get_test_execution({ executionId: \"<execution-id>\" }) until status is 'completed', then read the updated result with get_test_result."}

get_test_execution

Scope
compliance:read

Polls the status of a re-run started by rerun_test. Status flips pending → running → completed (or failed) with pass/fail counts; once 'completed', read the refreshed row with get_test_result.

Ask your AI client: “Did that test re-run finish? Check the execution status.”

Input

FieldTypeDescription
executionIdrequiredstring, 1–200The executionId returned by rerun_test.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_test_execution",    "arguments": {      "executionId": "<execution-id>"    }  }}

Result

json
{  "executionId": "<execution-id>",  "testId": "aws-s3-encryption",  "status": "completed",  "totalTests": 1,  "completedTests": 1,  "passingTests": 1,  "failingTests": 0,  "errorTests": 0,  "startedAt": "2026-05-27T14:30:00.000Z",  "completedAt": "2026-05-27T14:30:45.000Z",  "updatedAt": "2026-05-27T14:30:45.000Z"}

acknowledge_test

Scope
tests:write

PROPOSES a waiver on a failing test. It does NOT make the test pass or move the compliance score. It creates a PENDING request that a DIFFERENT human admin must approve in the dashboard before it applies. Two shapes: accept the risk on one flagged resource (testId + resourceId or findingId; accept-risk-eligible tests only), or waive the WHOLE check like the dashboard's "Waive this check" (testId + wholeTest + exceptionClass, on any failing check). exceptionClass records what management decided: alternative-control (another control meets the requirement, e.g. a PCI compensating control) or risk-accepted. The approver sees it before approving.

Ask your AI client: “Draft a risk-acceptance for the failing S3 encryption finding on the legacy exports bucket. We're decommissioning it in Q3.”

Input

FieldTypeDescription
testIdrequiredstring, 1–200The failing test (from list_tests / get_next_actions). Must be accept-risk-eligible.
resourceIdoptionalstring, 1–400The flagged resource to accept (from the test finding). One of resourceId / findingId is required unless wholeTest is set.
findingIdoptionalstring, 1–200Alternatively, the finding id (resolved + tenant-checked server-side).
rationalerequiredstring, 10–2000Why this risk is acceptable. Goes verbatim into the audit binder for the auditor.
affectedControlsoptionalstring[] (≤50)Optional control ids this waiver touches. Display-only context for the human approver.
proposedReviewByoptionalstring, 1–40Optional re-review date (YYYY-MM-DD).Default: +12 months
wholeTestoptionaltrueWaive the whole check instead of one resource. Requires exceptionClass; names no resourceId/findingId.
exceptionClassoptional'alternative-control' | 'risk-accepted'Required with wholeTest. Ask the user which; it is recorded verbatim.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "acknowledge_test",    "arguments": {      "testId": "aws-s3-encryption",      "resourceId": "s3://acme-legacy-exports",      "rationale": "Bucket holds only public marketing assets; decommission scheduled for Q3."    }  }}

Result

json
{  "requestId": "<request-id>",  "state": "pending",  "proposedReviewBy": "2027-05-27T16:00:00.000Z",  "message": "Proposed an accept-risk waiver for 'aws-s3-encryption'. This has NOT changed the score. A different admin must approve it in the dashboard before it applies."}

exclude_from_scope

Scope
tests:write

Does what the dashboard's "Exclude from scope" button does: the resource (a decommissioned S3 bucket, an archived repository, a non-production environment, a retired laptop) leaves the population of every check that reads it. It does NOT fix the finding, and each affected check tells auditors what was excluded and why. Take integrationType, resourceKind and resourceId from the scopeOut handle get_test_result returns on a failing resource; for Kandji and Jamf the kind is device. Owner/admin only. Queues a re-check of the affected checks. Undo with restore_to_scope.

Ask your AI client: “The legacy exports bucket is being deleted. Take it out of scope for the S3 checks.”

Input

FieldTypeDescription
inventoryKeyoptionalstringAn Inventory row's key from list_inventory, for an asset no check names (a manually-added asset, an inventory-only resource). Use it instead of integrationType / resourceKind / resourceId; reason is still required.
integrationTypeoptionalstringThe integration the resource belongs to (aws, github, kandji, …). From scopeOut.integrationType. Required unless inventoryKey is given.
resourceKindoptionalstringThe kind (s3-bucket, repository, environment, device, …). From scopeOut.resourceKind. Required unless inventoryKey is given.
resourceIdoptionalstring, 1–256The provider's immutable id of ONE resource. One of resourceId / resourceIds is required.
resourceIdsoptionalstring[] (≤200)Several resources of the same kind. Requires reasonCategory.
reasonrequiredstring, 10–2000Why the resource is out of scope. Shown to auditors on every affected check.
reasonCategoryoptionalstringdecommissioned, non-production, managed-by-third-party, outside-audit-boundary, duplicate-record or other (devices: retired, lost-or-stolen, wiped-at-offboarding, never-an-endpoint, duplicate-record, other). Required for a batch and for some kinds.
resourceNameoptionalstringDisplay label for a single resource. The key is always derived from resourceId.
aiReasoningoptionalstring, 1–2000Optional internal reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "exclude_from_scope",    "arguments": {      "integrationType": "aws",      "resourceKind": "s3-bucket",      "resourceId": "acme-legacy-exports",      "reason": "Bucket is being deleted this week; it holds no customer data.",      "reasonCategory": "decommissioned"    }  }}

Result

json
{  "op": "excluded",  "integration": "AWS",  "integrationType": "aws",  "resourceKind": "s3-bucket",  "requested": 1,  "succeeded": [    {      "resourceId": "acme-legacy-exports",      "resourceKey": "arn:aws:s3:::acme-legacy-exports"    }  ],  "failed": [],  "recheck": {    "queued": true,    "executionId": "<execution-id>"  },  "note": "The S3 bucket leaves the population of the AWS checks that read it. This does not fix any finding; each check states the exclusion and its reason to auditors. …"}

restore_to_scope

Scope
tests:write

Does what the dashboard's "Restore" does: the checks that read the resource evaluate it again and its findings count again. Take the ids from list_scope_exclusions. Notes somebody recorded on the asset survive, and a device Strac excluded automatically stays restored. Owner/admin only. Queues a re-check.

Ask your AI client: “We kept the legacy exports bucket after all. Put it back in scope.”

Input

FieldTypeDescription
inventoryKeyoptionalstringAn excluded Inventory row's key from list_inventory (includeScopedOut: true). Use it instead of integrationType / resourceKind / resourceId.
integrationTypeoptionalstringFrom list_scope_exclusions. Required unless inventoryKey is given.
resourceKindoptionalstringFrom list_scope_exclusions. Required unless inventoryKey is given.
resourceIdoptionalstring, 1–256One excluded resource. One of resourceId / resourceIds is required.
resourceIdsoptionalstring[] (≤200)Several excluded resources of the same kind.
aiReasoningoptionalstring, 1–2000Optional internal reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "restore_to_scope",    "arguments": {      "integrationType": "aws",      "resourceKind": "s3-bucket",      "resourceId": "acme-legacy-exports"    }  }}

Result

json
{  "op": "restored",  "integration": "AWS",  "integrationType": "aws",  "resourceKind": "s3-bucket",  "requested": 1,  "succeeded": [    {      "resourceId": "acme-legacy-exports",      "resourceKey": "arn:aws:s3:::acme-legacy-exports"    }  ],  "failed": [],  "recheck": {    "queued": true,    "executionId": "<execution-id>"  }}

list_test_evidence

Scope
compliance:read

Lists the reference/audit files attached to a test (uploaded via begin_test_evidence_upload). Pass the testResultId from list_tests / get_test_result. Download URLs are omitted by default. Pass includeDownloadUrls:true only when you actually need to fetch a file (the presigned URL is a bearer-less link that lands in the conversation).

Ask your AI client: “What audit files are attached to the S3 encryption test?”

Input

FieldTypeDescription
testResultIdrequiredstring, 1–200The id of the test result row from list_tests / get_test_result.
includeDownloadUrlsoptionalbooleanWhen true, include a short-lived presigned download URL per evidence file.Default: false

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_test_evidence",    "arguments": {      "testResultId": "<result-id>"    }  }}

Result

json
{  "testResultId": "<result-id>",  "testId": "aws-s3-encryption",  "evidence": [    {      "evidenceId": "<evidence-id>",      "name": "Q1 access review export",      "notes": "Reviewed by security team.",      "contentType": "application/pdf",      "uploadedBy": "admin@example.com",      "createdAt": "2026-05-27T16:05:00.000Z",      "fileSize": 248192    }  ]}

begin_test_evidence_upload

Scope
evidence:write

Step 1 of the test-evidence handshake. Attaches a reference/audit file (PDF, screenshot, export; ≤25 MB, any type) to ONE automated test: the file an auditor reviews alongside the result. Pass the testResultId from list_tests / get_test_result (NOT the testId field). Returns a short-lived S3 PUT URL; upload the raw bytes with the returned `next` curl command, then call finalize_test_evidence_upload. This does NOT link evidence to a control (use attach_evidence) and does NOT change the pass/fail status or compliance score of the test.

Ask your AI client: “Attach this access-review PDF to the access review test as audit evidence.”

Input

FieldTypeDescription
testResultIdrequiredstring, 1–200The id of the test result row from list_tests / get_test_result (the testResultId field, NOT the secondary testId).
namerequiredstring, 1–256Human-readable evidence name, e.g. 'Q1 access review export'.
contentTyperequiredstring, 1–128MIME type of the file (e.g. application/pdf, image/png). Must match the Content-Type sent on the PUT.
notesoptionalstring, ≤2048Optional context for auditors.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_test_evidence_upload",    "arguments": {      "testResultId": "<result-id>",      "name": "Q1 access review export",      "contentType": "application/pdf"    }  }}

Result

json
{  "uploadId": "<upload-id>",  "uploadUrl": "https://<bucket>.s3.amazonaws.com/test-evidence/_staging/comp_demo/<evidence-id>?X-Amz-Signature=<sig>",  "uploadExpiresAt": "2026-05-27T16:10:00.000Z",  "maxBytes": 26214400,  "contentType": "application/pdf",  "next": "Upload the file bytes directly to S3, then finalize:\n  1. curl -X PUT -T <path-to-file> -H 'Content-Type: application/pdf' '<uploadUrl>'\n  2. call finalize_test_evidence_upload with this uploadId"}

finalize_test_evidence_upload

Scope
evidence:write

Step 2 of the test-evidence handshake. After you PUT the file bytes to the URL from begin_test_evidence_upload, pass the uploadId; the server size-checks the object (≤25 MB, non-empty) and registers it against the test. Idempotent: a repeated call with the same uploadId returns the same result. Returns precondition_failed if the file was not uploaded first. The returned `note` confirms the file is reference/audit material only and does NOT change the test status or compliance score.

Ask your AI client: “Finalize the test-evidence upload. Here is the uploadId.”

Input

FieldTypeDescription
uploadIdrequiredstring, 1–80The uploadId returned by begin_test_evidence_upload.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "finalize_test_evidence_upload",    "arguments": {      "uploadId": "<upload-id>"    }  }}

Result

json
{  "evidenceId": "<evidence-id>",  "name": "Q1 access review export",  "testId": "aws-s3-encryption",  "status": "uploaded",  "fileSize": 248192,  "note": "Stored as reference/audit material. Does NOT change the test status or compliance score."}

assign_test_owner

Scope
tests:write

Sets the per-test owner of checks on the Tests page: the person each check emails when it starts failing or its vulnerabilities pass their deadline. Owner/admin. `owner` must be an active member's email (list them with get_vulnerability_reminder_settings includeMembers) or null to clear; a role or team name is refused. Only checks list_tests returns are accepted (up to 200 ids); any other id is a per-row error. A check already owned by that person is reported unchanged. The new owner gets one notice naming the checks, unless you assign yourself. Ids not reached in the time budget come back as `notAttempted`.

Ask your AI client: “Make cloud@example.com the owner of our AWS vulnerability checks.”

Input

FieldTypeDescription
testIdsrequiredstring[], 1–200Check ids from list_tests.
ownerrequiredstring | nullAn active member's email, or null to clear the per-test owner.
aiReasoningoptionalstring, ≤2000Optional reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "assign_test_owner",    "arguments": {      "testIds": [        "aws-vuln-critical",        "aws-vuln-high"      ],      "owner": "cloud@example.com"    }  }}

Result

json
{  "organizationId": "comp_demo",  "owner": "cloud@example.com",  "written": [    "aws-vuln-critical"  ],  "unchanged": [    "aws-vuln-high"  ],  "errors": [],  "notified": true,  "note": "cloud@example.com was notified once for the 1 newly assigned check."}

set_vulnerability_reminder_settings

Scope
tests:write

Changes Settings > Notifications > vulnerability deadline alerts, TIGHTEN ONLY. Owner/admin. Read the settings first and pass `settings.updatedAt` as `expectedUpdatedAt`; send only the fields you change. Allowed: turning the reminder on, widening recipients or severity, a longer heads-up window, a more frequent repeat, adding active members to `alsoNotify` (the list replaces the stored one), and setting a Slack channel when none is set. Turning it off, narrowing, removing any address from `alsoNotify`, or clearing or moving the Slack channel is refused with nothing written (`_meta['strac.io/errorReason']` = `loosening_refused`); a person makes that change on the dashboard, and there is no confirm option. Every write, and every refused loosening, is in the MCP audit log. A timeout returns `outcome_unknown`: re-read and compare before retrying.

Ask your AI client: “Remind us daily about overdue vulnerabilities, and warn two weeks before a deadline.”

Input

FieldTypeDescription
expectedUpdatedAtoptionalstring | nullsettings.updatedAt from get_vulnerability_reminder_settings. Omit only when nothing is saved yet.
enabledoptionalbooleantrue turns the reminder on. false is refused.
recipientsoptional'admins-and-owners' | 'owners''admins-and-owners' widens who hears it; 'owners' is refused.
severityoptional'critical-and-high' | 'critical''critical-and-high' widens it; 'critical' is refused.
dueSoonDaysoptionalinteger, 0–30Heads-up window before a deadline. Only a larger value is accepted.
repeatoptional'daily' | 'weekly' | 'never'How often a still-overdue finding is reminded. Only a more frequent value is accepted.
alsoNotifyoptionalstring[], ≤10The full list of extra member addresses. Keep every stored address and add new ones: removing any is refused.
slackChannelIdoptionalstring | nullA channel id from includeSlackChannels, only when no channel is set. The Strac app must be in the channel.
aiReasoningoptionalstring, ≤2000Optional reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "set_vulnerability_reminder_settings",    "arguments": {      "expectedUpdatedAt": "2026-10-14T00:00:00.000Z",      "repeat": "daily",      "dueSoonDays": 14    }  }}

Result

json
{  "organizationId": "comp_demo",  "written": true,  "changed": [    "dueSoonDays",    "repeat"  ],  "settings": {    "enabled": true,    "recipients": "admins-and-owners",    "severity": "critical-and-high",    "dueSoonDays": 14,    "repeat": "daily",    "alsoNotify": [],    "slackChannel": null,    "updatedAt": "2026-10-19T09:00:00.000Z"  },  "note": "Saved. The next daily reminder run uses these settings."}

Evidence & lifecycle (write)

Write evidence and drive the policy/document lifecycle. Every write is attributed in the append-only audit log.

mark_control_na

Scope
evidence:write

Marks a control N/A with a reason (the auditor sees it). Re-marking preserves the original marker.

Ask your AI client: “Mark CC6.1 not applicable. We don't issue laptops.”

Input

FieldTypeDescription
controlIdrequiredstring, 1–120Control id, e.g. CC6.1.
reasonrequiredstring, 10–500Why the control does not apply.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "mark_control_na",    "arguments": {      "controlId": "CC6.1",      "reason": "No customer-managed laptops; access is SSO-only on managed devices."    }  }}

Result

json
{  "controlId": "CC6.1",  "naReason": "No customer-managed laptops…",  "markedNAAt": "2026-05-27T16:00:00.000Z",  "markedNABy": "admin@example.com",  "remarkedAt": null,  "remarkedBy": null}

mark_evidence_na

Scope
evidence:write

Marks a single evidence slot: a canonical catalog document, a policy, or a required "connect an automated evidence source" integration slot on one control. N/A with a customer-visible justification (the evidence twin of mark_control_na; reversible with unmark_evidence_na or the web UI). The slot DROPS from the required-evidence checklist of every control it belongs to (leaves both numerator and denominator), raising that per-control completion %; an integration waiver also lifts the readiness gap it was forcing. The framework headline moves only if the N/A flips a control to ready. Audit-logged and admin-notified.

Ask your AI client: “Mark the Physical Security Evidence document not applicable. We are fully remote.”

Input

FieldTypeDescription
evidenceTyperequired'document' | 'policy' | 'integration'Which kind of slot: 'document' (catalog document), 'policy', or 'integration' (an integration-coverage slot on one control).
evidenceIdrequiredstring, 1–200Catalog document id (from list_documents), policy id (from list_policies), or the integration-coverage item id from the control's required-evidence checklist.
controlIdoptionalstring, 1–200Required when evidenceType is 'integration': the control the slot belongs to (e.g. soc2-cc1.3). Rejected for the other kinds.
reasonrequiredstring, 10–500Customer-visible justification, shown in the N/A badge and the audit binder.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log row (not user-visible).

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "mark_evidence_na",    "arguments": {      "evidenceType": "document",      "evidenceId": "DOC-CC6-PHYSICAL-SECURITY",      "reason": "Fully remote company with no offices or owned data centres; physical controls are inherited from the cloud provider."    }  }}

Result

json
{  "evidenceType": "document",  "evidenceId": "DOC-CC6-PHYSICAL-SECURITY",  "lifecycleStatus": "not_applicable",  "naReason": "Fully remote company with no offices or owned data centres…",  "markedBy": "admin@example.com",  "markedAt": "2026-06-05T16:00:00.000Z"}

Document shape shown. A policy slot instead returns { evidenceType: "policy", evidenceId, lifecycleStatus, naReason, markedNAAt, markedNABy }. Re-marking preserves the original marker. An integration slot returns { evidenceType: "integration", evidenceId, controlId, lifecycleStatus, naReason, markedBy, markedAt }. Use it only when none of the candidate sources the slot lists is (or will be) part of your stack; connecting one, or uploading manual evidence against the slot, are the alternatives.

unmark_control_na

Scope
evidence:write

The undo of mark_control_na. The control re-enters completion % and its readiness verdict is recomputed from its evidence. Idempotent (a control that is not N/A returns wasNotApplicable: false). Every other decision recorded on the control (compensating-control worksheet, owner, note, slot waivers, SoA and HIPAA determinations) is preserved. Audit-logged and admin-notified.

Ask your AI client: “We issue laptops again. Put CC6.1 back in scope.”

Input

FieldTypeDescription
controlIdrequiredstring, 1–120Canonical control id from list_controls, e.g. soc2-cc6.1.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "unmark_control_na",    "arguments": {      "controlId": "soc2-cc6.1"    }  }}

Result

json
{  "controlId": "soc2-cc6.1",  "wasNotApplicable": true,  "restoredAt": "2026-09-21T10:00:00.000Z",  "restoredBy": "admin@example.com",  "previousReason": "No customer-managed laptops…"}

unmark_evidence_na

Scope
evidence:write

The undo of mark_evidence_na, for the same three slot kinds. The slot re-enters the required-evidence checklist of every control it belongs to, so per-control completion % can go down. A document or policy slot returns to published, draft or not-started according to the versions it still holds; nothing is re-approved or re-published. Idempotent. Audit-logged and admin-notified.

Ask your AI client: “We opened an office. Put the Physical Security Evidence document back in scope.”

Input

FieldTypeDescription
evidenceTyperequired'document' | 'policy' | 'integration'Which kind of slot to restore.
evidenceIdrequiredstring, 1–200Catalog document id, policy id, or the integration-coverage item id.
controlIdoptionalstring, 1–200Required for evidenceType=integration: the control the slot belongs to.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "unmark_evidence_na",    "arguments": {      "evidenceType": "document",      "evidenceId": "DOC-CC6-PHYSICAL-SECURITY"    }  }}

Result

json
{  "evidenceType": "document",  "evidenceId": "DOC-CC6-PHYSICAL-SECURITY",  "wasNotApplicable": true,  "lifecycleStatus": "published",  "currentVersionId": "ver-…",  "nextReviewDate": "2027-09-21T10:00:00.000Z",  "previousReason": "Fully remote company…"}

attach_evidence

Scope
evidence:write

Links existing evidence to a control. Idempotent (re-attaching is a no-op). Fans out to submitted audit binders.

Ask your AI client: “Attach policy POL-SEC-001 as evidence for CC6.1.”

Input

FieldTypeDescription
controlIdrequiredstring, 1–120Control id, e.g. CC6.1.
evidenceTyperequiredstringKind of evidence.policydocument
referenceIdrequiredstring, 1–120Policy id or document id to attach.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "attach_evidence",    "arguments": {      "controlId": "CC6.1",      "evidenceType": "policy",      "referenceId": "POL-SEC-001"    }  }}

Result

json
{  "controlId": "CC6.1",  "evidenceType": "policy",  "referenceId": "POL-SEC-001",  "attached": true,  "noop": false,  "totalControlsLinked": 4}

upload_document

Scope
documents:write

Uploads a base64 document (≤4 MB raw) that appears in list_documents immediately. No-orphan rule: controlIds is REQUIRED (≥1). An ad-hoc document with no control links counts toward nothing.

Ask your AI client: “Upload this pen test report as a compliance document.”

Input

FieldTypeDescription
titlerequiredstring, 1–200Document title.
descriptionoptionalstring, 0–2000Optional description.
contentTyperequiredMIME enumOne of the allowed types: pdf, docx, doc, xlsx, xls, pptx, ppt, png, jpeg, gif, webp, heic, heif, tiff, bmp, markdown, plain, csv, json, eml, rtf, zip, tar, gzip, 7z, rar.
contentBase64requiredstring (base64)Base64 bytes, ≤4 MB after decode.
controlIdsrequiredstring[], 1–20control ids stored on the document (e.g. ['soc2-cc6.1']). At least one is required. For completion %, also call attach_evidence per control.
frameworksoptionalstring[], cap 10additional framework tags stored on the document. Use the exact labels 'SOC 2', 'ISO 27001', 'NIST CSF 2.0', 'PCI DSS', 'HIPAA' (an unrecognized label hides the document in the web list).
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "upload_document",    "arguments": {      "title": "Pen Test Report 2026",      "contentType": "application/pdf",      "contentBase64": "JVBERi0xLjcK...<base64>",      "controlIds": [        "soc2-cc4.1"      ]    }  }}

Result

json
{  "documentId": "<doc-uuid>",  "title": "Pen Test Report 2026",  "contentType": "application/pdf",  "sizeBytes": 184523,  "sha256": "<hex>",  "s3Key": "mcp/<doc-uuid>/pen-test-report-2026.pdf",  "uploadedAt": "2026-05-27T16:00:00.000Z"}

upload_policy

Scope
policies:write

Creates the FIRST version of a canonical policy. Provide exactly one of `markdown` or (`fileName` + `fileContent`). Returns 409 if a version already exists (use update_policy).

Ask your AI client: “Upload the first version of policy POL-SEC-001 from this markdown.”

Input

FieldTypeDescription
policyIdrequiredstring, 1–120Canonical policy id, e.g. POL-SEC-001.
markdownoptionalstringPolicy body as markdown (sanitized). Mutually exclusive with file fields.
fileNameoptionalstring, 1–200File name with extension. Pair with fileContent.
fileContentoptionalstring (base64), ≤4 MB rawBase64 file bytes. Pair with fileName.
commentsoptionalstring, 0–2000Version changelog note.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "upload_policy",    "arguments": {      "policyId": "POL-SEC-001",      "markdown": "# Information Security Policy\n\n## Purpose\n..."    }  }}

Result

json
{  "policyId": "POL-SEC-001",  "versionId": "<version-uuid>",  "version": 1,  "uploadedAt": "2026-05-27T16:00:00.000Z",  "uploadedBy": "ciso@example.com",  "status": "draft",  "contentFormat": "md",  "fileName": null,  "sizeBytes": 4096}

update_policy

Scope
policies:write

Adds a new draft version. `basedOnVersion` must equal the current version counter; on mismatch it returns a structured `{ stale: true }` result (not an error) so you can refetch. `force: true` bypasses the check.

Ask your AI client: “Update POL-SEC-001 with these revisions (based on version 3).”

Input

FieldTypeDescription
policyIdrequiredstring, 1–120Policy id.
basedOnVersionrequiredpositive integerThe version counter you edited from (optimistic lock).
markdownoptionalstringNew body (markdown). Mutually exclusive with file fields.
fileNameoptionalstring, 1–200File name with extension. Pair with fileContent.
fileContentoptionalstring (base64), ≤4 MB rawBase64 file bytes.
commentsoptionalstring, 0–2000Version changelog note.
forceoptionalbooleanSkip the stale-version check.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "update_policy",    "arguments": {      "policyId": "POL-SEC-001",      "basedOnVersion": 3,      "markdown": "# Information Security Policy (rev 4)\n..."    }  }}

Result

json
{  "policyId": "POL-SEC-001",  "versionId": "<version-uuid>",  "version": 4,  "basedOnVersion": 3,  "uploadedAt": "2026-05-27T16:00:00.000Z",  "uploadedBy": "ciso@example.com",  "status": "draft",  "contentFormat": "md",  "fileName": null,  "sizeBytes": 4210}

compare_policy

Scope
policies:read

Deterministic (no LLM) section diff of a policy version against its canonical required sections. Defaults to the current/latest version.

Ask your AI client: “Does our current POL-SEC-001 cover all the required sections?”

Input

FieldTypeDescription
policyIdrequiredstring, 1–120Policy id.
versionIdoptionalstring, 1–120Specific version; defaults to current/latest.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "compare_policy",    "arguments": {      "policyId": "POL-SEC-001"    }  }}

Result

json
{  "policyId": "POL-SEC-001",  "policyTitle": "Information Security Policy",  "versionId": "<version-uuid>",  "version": 3,  "versionStatus": "approved",  "resolvedFormat": "pdf",  "conversionWarnings": [],  "diff": {    "missingSections": [      "Access Review Cadence"    ],    "extraSections": [],    "sectionDeltas": [      {        "section": "Acceptable Use",        "matchedTitle": "Acceptable Use",        "bodyLengthChars": 842,        "appearsToBePlaceholder": false      }    ],    "detectedHeadingStyle": "atx",    "totalPolicySections": 7  }}

approve_policy

Scope
policies:approve

Approves a version and flips the policy to published. Idempotent (re-approving is a no-op preserving the original approver).

Ask your AI client: “Approve version <version-uuid> of POL-SEC-001.”

Input

FieldTypeDescription
versionIdrequiredstring, 1–120The version id to approve.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "approve_policy",    "arguments": {      "versionId": "<version-uuid>"    }  }}

Result

json
{  "versionId": "<version-uuid>",  "policyId": "POL-SEC-001",  "version": 4,  "approvedAt": "2026-05-27T16:05:00.000Z",  "approvedBy": "ceo@example.com",  "noop": false}

record_policy_review

Scope
policies:approve

Records that a policy's current approved version was reviewed and is still accurate. This restarts its review window so it keeps, or resumes, counting as evidence. There is no new version, no new approval, and no new acknowledgment request to employees. Only for an approved, published policy whose review is due: the review date has passed or is within 30 days (list_policies returns reviewable: true). A policy covered by another policy or document is refused; review the source instead. If the text changed, draft and approve a new version instead. Pass the versionId you read, so a newer approval is never attested by mistake. Owner or admin tokens only. The reviewer is your identity, never an argument, and the auditor sees the review with your name and the date.

Ask your AI client: “Our Information Security Policy is overdue but nothing in it changed. Mark it reviewed.”

Input

FieldTypeDescription
policyIdrequiredstring, 1–120The policy id (list_policies → policyId), e.g. POL-SEC-001.
versionIdoptionalstring, 1–120The approved version you reviewed. Refused if a newer version was approved since.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "record_policy_review",    "arguments": {      "policyId": "POL-SEC-001",      "versionId": "<version-uuid>"    }  }}

Result

json
{  "policyId": "POL-SEC-001",  "versionId": "<version-uuid>",  "reviewedAt": "2026-09-24T10:00:00.000Z",  "reviewedBy": "ciso@example.com",  "previousNextReviewDate": "2026-09-18T00:00:00.000Z",  "nextReviewDate": "2027-09-24T10:00:00.000Z"}

Refused (precondition_failed) for a policy with no approved version, one not yet due, a covered slot, or when versionId is no longer the approved head.

create_custom_policy

Scope
policies:write

One call to mint a CUSTOM-POL-* policy, its first draft version, and its control mappings. All controlIds are validated before the write. Provide exactly one of `markdown` or (`fileName` + `fileContent`).

Ask your AI client: “Create a custom Incident Response Policy mapped to CC7.4.”

Input

FieldTypeDescription
titlerequiredstring, 1–200Policy title.
descriptionoptionalstring, 0–2000Optional description.
categoryoptionalstring, 0–80Optional category.
frameworksoptionalstring[] (≤10)Framework tags.
controlIdsoptionalstring[] (≤100)Controls to map; each must exist.
markdownoptionalstringBody as markdown. Mutually exclusive with file fields.
fileNameoptionalstring, 1–200File name with extension. Pair with fileContent.
fileContentoptionalstring (base64), ≤4 MB rawBase64 file bytes.
commentsoptionalstring, 0–2000Version changelog note.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "create_custom_policy",    "arguments": {      "title": "Incident Response Policy",      "frameworks": [        "SOC 2"      ],      "controlIds": [        "CC7.4"      ],      "markdown": "# Incident Response Policy\n..."    }  }}

Result

json
{  "policyId": "CUSTOM-POL-<uuid>",  "title": "Incident Response Policy",  "frameworks": [    "SOC 2"  ],  "controlIds": [    "CC7.4"  ],  "versionId": "<version-uuid>",  "version": 1,  "uploadedAt": "2026-05-27T16:00:00.000Z",  "uploadedBy": "ciso@example.com",  "status": "draft",  "contentFormat": "md",  "fileName": null,  "sizeBytes": 2048}

assign_control_owner

Scope
evidence:write

Sets the owner of controls (the Owner column on the dashboard; the person their reminders go to), through the same write as the dashboard. Owner/admin. `owner` must be an active member's email (list them with get_vulnerability_reminder_settings includeMembers) or null to clear; a role or team name is refused. Up to 200 ids; an unknown id is a per-row `not_found` error. A control already owned by that person is reported unchanged and nobody is notified. The new owner gets one notice naming the records, unless you assign yourself. Ids not reached in the time budget come back as `notAttempted`.

Ask your AI client: “Make security@example.com the owner of our SOC 2 access controls.”

Input

FieldTypeDescription
idsrequiredstring[], 1–200Control ids from list_controls.
ownerrequiredstring | nullAn active member's email, or null to clear the owner.
aiReasoningoptionalstring, ≤2000Optional reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "assign_control_owner",    "arguments": {      "ids": [        "soc2-cc6.1",        "soc2-cc6.2"      ],      "owner": "security@example.com"    }  }}

Result

json
{  "organizationId": "comp_demo",  "owner": "security@example.com",  "written": [    "soc2-cc6.1"  ],  "unchanged": [    "soc2-cc6.2"  ],  "errors": [],  "notified": true,  "note": "security@example.com was notified once for the 1 newly assigned control."}

assign_policy_owner

Scope
policies:write

Sets the owner of policies (the Owner column on the dashboard; the person their reminders go to), through the same write as the dashboard. Owner/admin. `owner` must be an active member's email (list them with get_vulnerability_reminder_settings includeMembers) or null to clear; a role or team name is refused. Up to 200 ids; an unknown id is a per-row `not_found` error. A policy already owned by that person is reported unchanged and nobody is notified. The new owner gets one notice naming the records, unless you assign yourself. Ids not reached in the time budget come back as `notAttempted`.

Ask your AI client: “Assign the information security policy to security@example.com.”

Input

FieldTypeDescription
idsrequiredstring[], 1–200Policy ids from list_policies.
ownerrequiredstring | nullAn active member's email, or null to clear the owner.
aiReasoningoptionalstring, ≤2000Optional reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "assign_policy_owner",    "arguments": {      "ids": [        "POL-SEC-001",        "POL-HR-001"      ],      "owner": "security@example.com"    }  }}

Result

json
{  "organizationId": "comp_demo",  "owner": "security@example.com",  "written": [    "POL-SEC-001"  ],  "unchanged": [    "POL-HR-001"  ],  "errors": [],  "notified": true,  "note": "security@example.com was notified once for the 1 newly assigned policy."}

assign_document_owner

Scope
documents:write

Sets the owner of documents (the Owner column on the dashboard; the person their reminders go to), through the same write as the dashboard. Owner/admin. `owner` must be an active member's email (list them with get_vulnerability_reminder_settings includeMembers) or null to clear; a role or team name is refused. Up to 200 ids; an unknown id is a per-row `not_found` error. A document already owned by that person is reported unchanged and nobody is notified. The new owner gets one notice naming the records, unless you assign yourself. A restricted (sensitive) document can be assigned only by the person it is restricted to. Ids not reached in the time budget come back as `notAttempted`.

Ask your AI client: “Make security@example.com the owner of the org chart document.”

Input

FieldTypeDescription
idsrequiredstring[], 1–200Document ids from list_documents: a catalog slot id (DOC-…) or a custom document id.
ownerrequiredstring | nullAn active member's email, or null to clear the owner.
aiReasoningoptionalstring, ≤2000Optional reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "assign_document_owner",    "arguments": {      "ids": [        "DOC-CC1-ORG-CHART",        "DOC-CC7-INCIDENT-REPORT"      ],      "owner": "security@example.com"    }  }}

Result

json
{  "organizationId": "comp_demo",  "owner": "security@example.com",  "written": [    "DOC-CC1-ORG-CHART"  ],  "unchanged": [    "DOC-CC7-INCIDENT-REPORT"  ],  "errors": [],  "notified": true,  "note": "security@example.com was notified once for the 1 newly assigned document."}

set_completion_alert_settings

Scope
evidence:write

Changes the completion-drop alert settings, TIGHTEN ONLY. Owner/admin. Pass the `updatedAt` from get_completion_alert_settings as `expectedUpdatedAt` (omit it only when nothing is saved yet) and only the fields you change. Allowed: turning the crossing alert on (`belowPercent` from 0 to a level) and a smaller `dropPoints`. Any other `belowPercent` change (a crossing fires only when a framework starts at or above the level, so moving it either way drops alerts the current level sends) or a larger `dropPoints` is refused with nothing written (`loosening_refused`); a person makes those changes in Settings > Notifications, and there is no confirm option. Every write, and every refused loosening, is in the MCP audit log and notifies the admins in the app.

Ask your AI client: “Email us when any framework drops 3 points or more.”

Input

FieldTypeDescription
expectedUpdatedAtoptionalstring | nullsettings.updatedAt from get_completion_alert_settings.
belowPercentoptionalinteger, 0–100Alert when a framework crosses below this %. Accepted only when it is 0 (off) now.
dropPointsoptionalinteger, 1–100Alert when a framework falls at least this many points. Only a smaller value is accepted.
aiReasoningoptionalstring, ≤2000Optional reasoning for the MCP audit log.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "set_completion_alert_settings",    "arguments": {      "expectedUpdatedAt": "2026-10-12T15:00:00.000Z",      "dropPoints": 3    }  }}

Result

json
{  "organizationId": "comp_demo",  "written": true,  "changed": [    "dropPoints"  ],  "settings": {    "belowPercent": 80,    "dropPoints": 3,    "customized": true,    "updatedBy": "admin@example.com",    "updatedAt": "2026-10-20T15:00:00.000Z"  },  "note": "Saved. The next daily completion check uses these settings."}

Presigned file uploads (write)

Upload real .docx/.pdf bytes straight to S3 via a begin → PUT → finalize handshake, with no base64 through the model. Preferred over the inline upload_* tools for real files. The plural begin_*_uploads / finalize_*_uploads twins batch a whole folder in a handful of calls (200 URLs per begin, 20 finalizes per call) with per-row errors, dryRun previews, and inline approve/publish.

begin_policy_upload

Scope
policies:write

Step 1 of the presigned policy-upload handshake. Returns a short-lived S3 PUT URL. Upload the raw .docx/.doc/.pdf bytes directly, then call finalize_policy_upload. The original file is preserved (the dashboard renders markdown on read). Three modes: (1) `policyId` alone → first version of a canonical catalog policy; (2) `title` (+ optional custom fields) → a new CUSTOM-POL-* policy; (3) `policyId` + `basedOnVersion` → a new version of an EXISTING policy (canonical or custom) with the same optimistic-lock as update_policy. Prefer this over upload_policy / update_policy / create_custom_policy for real files where base64-through-the-model is unreliable.

Ask your AI client: “Upload this .docx as a new version of POL-SEC-001.”

Input

FieldTypeDescription
fileNamerequiredstring, ≤200File name with extension. Allowed: .docx, .doc, .pdf. Path-traversal-safe (no /, \, .., null bytes).
policyIdoptionalstring, 1–120Existing policy id: a canonical id from list_policies (e.g. POL-SEC-001) or a CUSTOM-POL-* id. Mutually exclusive with title. Add basedOnVersion to update an existing policy.
titleoptionalstring, 1–200Custom-policy title. Creates a new CUSTOM-POL-* policy. Mutually exclusive with policyId.
descriptionoptionalstring, 0–2000Custom policy: optional description.
categoryoptionalstring, 0–80Custom policy: optional category. Defaults to 'Custom'.
frameworksoptionalstring[] (≤10)Custom policy: optional framework codes, e.g. ['soc2'].
controlIdsoptionalstring[] (≤100)Custom policy: optional control codes to map; each must exist.
basedOnVersionoptionalpositive integerUpdate mode: the version number you were editing (from get_policy). Requires policyId. finalize_policy_upload returns a stale-version payload if the policy advanced since.
forceoptionalbooleanUpdate mode: skip the stale-version check. Use only after surfacing the conflict; defaulting it loses human edits silently. Requires basedOnVersion.
commentsoptionalstring, 0–2000Optional human-visible changelog comment.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_policy_upload",    "arguments": {      "policyId": "POL-SEC-001",      "fileName": "information-security-policy.docx"    }  }}

Result

json
{  "uploadId": "<upload-id>",  "uploadUrl": "https://<bucket>.s3.amazonaws.com/comp_demo/_staging/<upload-id>/information-security-policy.docx?X-Amz-Signature=<sig>",  "uploadExpiresAt": "2026-05-27T16:10:00.000Z",  "maxBytes": 52428800,  "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",  "next": "Upload the file bytes directly to S3, then finalize:\n  1. curl -X PUT -T <path-to-file> -H 'Content-Type: …' '<uploadUrl>'\n  2. call finalize_policy_upload with this uploadId"}

finalize_policy_upload

Scope
policies:write

Step 2 of the policy-upload handshake. After you PUT the file bytes to the URL from begin_policy_upload, pass the uploadId; the server reads the object (≤50 MB) and creates a draft version (first version, new custom policy, or, for an update begun with basedOnVersion, a new version of an existing policy). Idempotent. For an update, if the policy advanced since you read it, returns a structured { stale: true, latestCounter, … } payload WITHOUT consuming the upload. Re-read with get_policy, or re-call with force:true to override (no re-upload needed). The draft still needs approve_policy. Returns precondition_failed if the file was not uploaded first.

Ask your AI client: “Finalize the policy upload. Here is the uploadId.”

Input

FieldTypeDescription
uploadIdrequiredstring, 1–80The uploadId returned by begin_policy_upload.
forceoptionalbooleanUpdate mode only: override a stale-version conflict returned by a prior finalize, without re-uploading. Use only after surfacing the conflict. It can silently supersede a concurrent edit.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "finalize_policy_upload",    "arguments": {      "uploadId": "<upload-id>"    }  }}

Result

json
{  "policyId": "POL-SEC-001",  "versionId": "<version-uuid>",  "version": 1,  "uploadedAt": "2026-05-27T16:05:00.000Z",  "uploadedBy": "ciso@example.com",  "status": "draft",  "contentFormat": "docx",  "fileName": "information-security-policy.docx",  "sizeBytes": 248192}

begin_policy_uploads

Scope
policies:write

Bulk twin of begin_policy_upload: mints up to 200 presigned S3 PUT URLs in one call (chunk larger sets). Each row is the same shape as begin_policy_upload (`policyId` → existing policy; `title` → new custom; add `basedOnVersion` to update). Per-row errors[] (indexed by your input) keep a partial batch visible: one malformed row never blocks the others; dryRun:true previews per-row mode + canonical validity with zero writes. PUT the bytes in parallel, then call finalize_policy_uploads.

Ask your AI client: “Upload all 12 of these policy files into Strac in one batch.”

Input

FieldTypeDescription
policiesrequiredobject[], 1–200Upload specs, one per file: `fileName` (required) plus exactly one of `policyId` (existing canonical/custom) or `title` (new custom); optional description, category, frameworks, controlIds, basedOnVersion, force, comments. A malformed row lands in errors[].
dryRunoptionalbooleantrue → preview per-row mode + canonical validity with ZERO writes; no URLs minted.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_policy_uploads",    "arguments": {      "policies": [        {          "policyId": "POL-SEC-001",          "fileName": "information-security-policy.docx"        },        {          "title": "Data Retention Policy",          "fileName": "data-retention-policy.pdf"        }      ]    }  }}

Result

json
{  "dryRun": false,  "uploads": [    {      "index": 0,      "uploadId": "<upload-id-1>",      "uploadUrl": "https://<bucket>.s3.amazonaws.com/comp_demo/_staging/<upload-id-1>/information-security-policy.docx?X-Amz-Signature=<sig>",      "uploadExpiresAt": "2026-06-05T16:10:00.000Z",      "maxBytes": 52428800,      "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",      "next": "curl -X PUT -T <path-to-file> -H 'Content-Type: …' '<uploadUrl>', then finalize"    },    {      "index": 1,      "uploadId": "<upload-id-2>",      "uploadUrl": "https://<bucket>.s3.amazonaws.com/comp_demo/_staging/<upload-id-2>/data-retention-policy.pdf?X-Amz-Signature=<sig>",      "uploadExpiresAt": "2026-06-05T16:10:00.000Z",      "maxBytes": 52428800,      "contentType": "application/pdf",      "next": "curl -X PUT -T <path-to-file> -H 'Content-Type: …' '<uploadUrl>', then finalize"    }  ],  "errors": [],  "summary": "Minted 2 upload URLs, 0 failed",  "next": "PUT the bytes to each uploadUrl (in parallel), then call finalize_policy_uploads with all the uploadIds (it registers AND approves each draft)."}

finalize_policy_uploads

Scope
policies:write
+
policies:approve

Bulk twin of finalize_policy_upload: finishes up to 20 presigned policy uploads in one call (loop larger sets), registering each draft AND approving it inline so it counts toward completion immediately. Requires BOTH policies:write and policies:approve. Per-row results keyed by uploadId; a finalize failure lands in errors[], a row that finalizes but fails to approve returns approved:false WITH its draft versionId (re-run heals), and a stale update returns status 'stale' (re-read, or force via the single tool). Idempotent: re-running is safe.

Ask your AI client: “Finalize and approve the whole batch of policy uploads I just PUT.”

Input

FieldTypeDescription
uploadIdsrequiredstring[], 1–20, uniqueThe uploadIds returned by begin_policy_uploads (after the bytes were PUT). Max 20/call.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log rows.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "finalize_policy_uploads",    "arguments": {      "uploadIds": [        "<upload-id-1>",        "<upload-id-2>"      ]    }  }}

Result

json
{  "results": [    {      "uploadId": "<upload-id-1>",      "index": 0,      "status": "approved",      "policyId": "POL-SEC-001",      "versionId": "<version-uuid>",      "approved": true    },    {      "uploadId": "<upload-id-2>",      "index": 1,      "status": "approved",      "policyId": "CUSTOM-POL-<uuid>",      "versionId": "<version-uuid>",      "approved": true    }  ],  "errors": [],  "summary": "Finalized 2 (2 approved), 0 failed"}

begin_document_upload

Scope
documents:write

Step 1 of the presigned document-upload handshake. Returns a short-lived S3 PUT URL. Upload the raw bytes directly, then call finalize_document_upload. Same MIME allowlist as upload_document. Prefer this over upload_document for real files where base64-through-the-model is unreliable. Two modes: pass `title` for a new ad-hoc document (no-orphan rule: at least one of controlIds/frameworks is required), OR `documentId` (a canonical catalog slot id like DOC-CC2-SYSTEM-DESCRIPTION) to create a draft version of that catalog slot, then publish_document makes it count toward control completion.

Ask your AI client: “Upload this PDF pen test report as a document.”

Input

FieldTypeDescription
titleoptionalstring, 1–200Ad-hoc mode: human-readable document title. Mutually exclusive with documentId.
documentIdoptionalstring, 1–120Catalog mode: canonical catalog slot id (DOC-*). Creates a draft version of that slot. Publish with publish_document. Mutually exclusive with title.
fileNameoptionalstring, 1–200Catalog mode only: original file name (sanitized server-side). Defaults to "{documentId}.{ext}".
commentsoptionalstring, 0–2000Catalog mode only: human-visible changelog comment.
descriptionoptionalstring, 0–2000Ad-hoc mode only: optional description.
contentTyperequiredMIME enumMIME type of the file; must match the Content-Type sent on the PUT. Allowed: pdf, docx, doc, xlsx, xls, pptx, ppt, png, jpeg, gif, webp, heic, heif, tiff, bmp, markdown, plain, csv, json, eml, rtf, zip, tar, gzip, 7z, rar.
controlIdsoptionalstring[], cap 20Ad-hoc mode: control ids stored on the document (e.g. ['soc2-cc6.1']). At least one of controlIds/frameworks is REQUIRED on an ad-hoc upload (controlIds preferred); for completion %, also call attach_evidence per control.
frameworksoptionalstring[], cap 10Ad-hoc mode: framework tags stored on the document (e.g. ['SOC 2']). Frameworks-only is accepted for migration flows that attach controls afterward via attach_evidence.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_document_upload",    "arguments": {      "title": "Pen Test Report 2026",      "contentType": "application/pdf",      "controlIds": [        "soc2-cc4.1"      ]    }  }}

Result

json
{  "uploadId": "<upload-id>",  "uploadUrl": "https://<bucket>.s3.amazonaws.com/mcp-uploads/_staging/comp_demo/<upload-id>/pen-test-report-2026.pdf?X-Amz-Signature=<sig>",  "uploadExpiresAt": "2026-05-27T16:10:00.000Z",  "maxBytes": 52428800,  "contentType": "application/pdf",  "next": "Upload the file bytes directly to S3, then finalize:\n  1. curl -X PUT -T <path-to-file> -H 'Content-Type: application/pdf' '<uploadUrl>'\n  2. call finalize_document_upload with this uploadId"}

finalize_document_upload

Scope
documents:write

Step 2 of the document-upload handshake. After you PUT the file bytes to the URL from begin_document_upload, pass the uploadId; the server reads the object (≤50 MB) and registers it. Ad-hoc uploads (begun with title) return the documentId + S3 key + sha256 and appear in list_documents. Catalog uploads (begun with documentId) create a DRAFT version of the catalog slot and return its versionId. Call publish_document with that versionId so the document counts toward control completion. Idempotent: repeating with the same uploadId returns the same result. Returns precondition_failed if the file was not uploaded first.

Ask your AI client: “Finalize the document upload. Here is the uploadId.”

Input

FieldTypeDescription
uploadIdrequiredstring, 1–80The uploadId returned by begin_document_upload.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "finalize_document_upload",    "arguments": {      "uploadId": "<upload-id>"    }  }}

Result

json
{  "documentId": "<doc-uuid>",  "title": "Pen Test Report 2026",  "contentType": "application/pdf",  "sizeBytes": 184523,  "sha256": "<hex>",  "s3Key": "mcp/<doc-uuid>/pen-test-report-2026.pdf",  "s3VersionId": "<s3-version-id>",  "uploadedAt": "2026-05-27T16:05:00.000Z"}

Ad-hoc shape shown. A catalog upload (begun with documentId) instead returns { documentId, title, versionId, versionNumber, status: "draft", next }. Pass that versionId to publish_document.

begin_document_uploads

Scope
documents:write

Bulk twin of begin_document_upload: mints up to 200 presigned S3 PUT URLs in one call (chunk larger sets). Each row is the same shape as begin_document_upload (`title` → ad-hoc document; `documentId` → canonical catalog slot). Per-row errors[] (indexed by your input) keep a partial batch visible: one bad row (bad contentType, title+documentId both set, unknown slot) never blocks the others; dryRun:true previews the slot-match plan with zero writes. PUT the bytes in parallel, then call finalize_document_uploads.

Ask your AI client: “Batch-upload these 30 evidence PDFs into their catalog slots.”

Input

FieldTypeDescription
documentsrequiredobject[], 1–200Upload specs, one per file: `contentType` (required, same MIME allowlist as upload_document) plus exactly one of `title` (ad-hoc; requires at least one of controlIds/frameworks, per the no-orphan rule; optional description) or `documentId` (catalog slot like 'DOC-CC2-SYSTEM-DESCRIPTION'; optional fileName/comments). A malformed row lands in errors[].
dryRunoptionalbooleantrue → preview the slot-match plan (mode + catalog-slot validity) with ZERO writes; no URLs minted.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_document_uploads",    "arguments": {      "documents": [        {          "documentId": "DOC-CC2-SYSTEM-DESCRIPTION",          "contentType": "application/pdf"        },        {          "title": "Pen Test Report 2026",          "contentType": "application/pdf",          "controlIds": [            "soc2-cc4.1"          ]        }      ]    }  }}

Result

json
{  "dryRun": false,  "uploads": [    {      "index": 0,      "uploadId": "<upload-id-1>",      "uploadUrl": "https://<bucket>.s3.amazonaws.com/mcp-uploads/_staging/comp_demo/<upload-id-1>/DOC-CC2-SYSTEM-DESCRIPTION.pdf?X-Amz-Signature=<sig>",      "uploadExpiresAt": "2026-06-05T16:10:00.000Z",      "maxBytes": 52428800,      "contentType": "application/pdf",      "next": "curl -X PUT -T <path-to-file> -H 'Content-Type: application/pdf' '<uploadUrl>', then finalize"    },    {      "index": 1,      "uploadId": "<upload-id-2>",      "uploadUrl": "https://<bucket>.s3.amazonaws.com/mcp-uploads/_staging/comp_demo/<upload-id-2>/pen-test-report-2026.pdf?X-Amz-Signature=<sig>",      "uploadExpiresAt": "2026-06-05T16:10:00.000Z",      "maxBytes": 52428800,      "contentType": "application/pdf",      "next": "curl -X PUT -T <path-to-file> -H 'Content-Type: application/pdf' '<uploadUrl>', then finalize"    }  ],  "errors": [],  "summary": "Minted 2 upload URLs, 0 failed",  "next": "PUT the bytes to each uploadUrl (in parallel), then call finalize_document_uploads with all the uploadIds."}

finalize_document_uploads

Scope
documents:write

Bulk twin of finalize_document_upload: finishes up to 20 presigned document uploads in one call (loop larger sets). Catalog uploads (begun with a documentId) are registered AND published inline so the slot counts toward completion, with no separate publish_document call; ad-hoc uploads count on presence. Per-row results keyed by uploadId; a finalize failure lands in errors[], a catalog row that finalizes but fails to publish returns published:false WITH its draft versionId so a re-run heals it, and a re-run whose bytes match an archived version returns status 'superseded' (a newer version is live). Idempotent: re-running is safe.

Ask your AI client: “Finalize and publish the whole batch of document uploads I just PUT.”

Input

FieldTypeDescription
uploadIdsrequiredstring[], 1–20, uniqueThe uploadIds returned by begin_document_uploads (after the bytes were PUT). Max 20/call.
aiReasoningoptionalstring, 1–2000Optional rationale recorded on the audit-log rows.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "finalize_document_uploads",    "arguments": {      "uploadIds": [        "<upload-id-1>",        "<upload-id-2>"      ]    }  }}

Result

json
{  "results": [    {      "uploadId": "<upload-id-1>",      "index": 0,      "status": "published",      "documentId": "DOC-CC2-SYSTEM-DESCRIPTION",      "versionId": "<version-uuid>",      "published": true    },    {      "uploadId": "<upload-id-2>",      "index": 1,      "status": "finalized",      "documentId": "<doc-uuid>",      "published": false    }  ],  "errors": [],  "summary": "Finalized 2 (1 published, 1 ad-hoc), 0 failed"}

publish_document

Scope
documents:write

Publishes a draft catalog-document version (created via begin_document_upload with a documentId) so it counts toward control completion. This is the documents analog of approve_policy. Archives any older published version of the same slot and sets the next review date from the catalog cadence. Idempotent on already-published versions (no-op). publishedBy is derived from your identity, never from arguments.

Ask your AI client: “Publish the System Description draft I just uploaded.”

Input

FieldTypeDescription
versionIdrequiredstring, 1–120The catalog document version id from the finalize_document_upload response.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "publish_document",    "arguments": {      "versionId": "<version-uuid>"    }  }}

Result

json
{  "documentId": "DOC-CC2-SYSTEM-DESCRIPTION",  "versionId": "<version-uuid>",  "versionNumber": 1,  "publishedAt": "2026-06-05T16:05:00.000Z",  "publishedBy": "ciso@example.com",  "nextReviewDate": "2027-06-05T16:05:00.000Z",  "noop": false}

record_document_review

Scope
documents:write

Records that a living catalog document's current published version was reviewed and is still accurate. This renews its review window so it counts as evidence again, without a new upload or a new version. Only for living documents (an org chart, a retention schedule, a business continuity plan), and only when the review is due: the window has closed, or closes within 30 days. A periodic artifact such as a scan, an access review or a change log is refused, because only the new period's file renews it. Owner or admin tokens only. The reviewer is your identity, never an argument, and the auditor sees the review with your name and the date.

Ask your AI client: “Our data retention schedule is overdue but nothing in it changed. Mark it reviewed.”

Input

FieldTypeDescription
versionIdrequiredstring, 1–120The slot's current published version id (list_documents → currentVersionId).
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "record_document_review",    "arguments": {      "versionId": "<version-uuid>"    }  }}

Result

json
{  "documentId": "DOC-PCI-DATA-RETENTION-SCHEDULE",  "versionId": "<version-uuid>",  "reviewedAt": "2026-09-24T10:00:00.000Z",  "reviewedBy": "ciso@example.com",  "previousNextReviewDate": "2026-09-18T14:06:31.005Z",  "nextReviewDate": "2026-12-25T10:00:00.000Z"}

Refused (precondition_failed) for a periodic artifact, a document not yet due, a version that is no longer the one on file, or a slot covered by another document.

Q&A

Ask freeform questions grounded on your posture.

ask_compliance_question

Scope
chat:ask

Asks the compliance agent a question, grounded on your live posture. Read-only (no audit-log row). Echoes token usage for cost-aware follow-ups.

Ask your AI client: “What evidence do I still need for SOC 2 CC6?”

Input

FieldTypeDescription
messagerequiredstring, 1–8000Your question.
conversationHistoryoptionalarray (≤20 turns)Prior {role:"user"|"assistant", content} turns for context.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "ask_compliance_question",    "arguments": {      "message": "What evidence do I still need for SOC 2 CC6?"    }  }}

Result

json
{  "reply": "For CC6 you still need: an access-review document for Q2, and the MFA-enforcement test is failing on the AWS root account…",  "context": {    "testsPassing": 120,    "testsFailing": 18,    "policiesApproved": 12,    "integrationsConnected": 3,    "activeFrameworks": [      "soc2"    ]  },  "usage": {    "inputTokens": 1842,    "outputTokens": 240  }}

GRC migration (write)

Bulk-import a GRC-platform export (Vanta, Drata, Secureframe…): vendors, the risk register, and historic test runs (original collection timestamps preserved, so auditors can sample the pre-migration period). Idempotent and safe to re-run, plus the read-back tools the import uses to verify itself. The strac-import prompt (installed with the server) orchestrates the full migration end to end.

import_vendors

Scope
vendors:write

Bulk-create/enrich managed vendors from a GRC-platform export (Vanta, Drata, Secureframe…). Parse the export locally and send structured rows, max 200 per call. Dedupes by name: new names are created (race-safe, re-run idempotent); existing vendors are ENRICHED only on currently-empty fields. Human edits are never overwritten. Set dryRun: true first to preview created/updated/skipped without writing. Per-row errors[] (keyed by your input rows) make partial batches visible; a re-run heals them. Requires admin/owner.

Ask your AI client: “Import the vendors from my Vanta export. Preview first.”

Input

FieldTypeDescription
vendorsrequiredarray, 1–200 rowsVendor rows. Only `name` is required; optional fields include website, inherentRisk, category, securityOwner, businessOwner, businessPurpose, notes, dataAccess, compliance[], review dates, reviewFrequency, status.
dryRunoptionalbooleantrue → full preview (created/updated/skipped) with ZERO writes. Run first, show the customer, then commit.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "import_vendors",    "arguments": {      "vendors": [        {          "name": "Stripe",          "website": "stripe.com",          "inherentRisk": "high",          "securityOwner": "security@example.com"        }      ],      "dryRun": true    }  }}

Result

json
{  "importBatchId": "<batch-uuid>",  "dryRun": true,  "summary": "DRY RUN, would import: 1 created, 0 enriched, 0 skipped, 0 failed",  "created": [    {      "id": "vendor-imp-<sha16>",      "name": "Stripe"    }  ],  "updated": [],  "skipped": [],  "errors": [],  "warnings": []}

import_risks

Scope
risks:write

Bulk-create risk-register entries from a GRC-platform export (e.g. Vanta's Risk Register CSV), max 200 per call. Every row needs the CC3 minimum: treatment, nextReviewDate, isFraudRisk. treatment='accept' rows record YOU as the acceptance approver (server-derived, never an argument). residual > inherent is clamped down with a warning. Dedupes by title; dryRun previews; per-row errors[]; re-runs heal. Requires admin/owner.

Ask your AI client: “Import my Vanta risk register CSV into Strac Comply.”

Input

FieldTypeDescription
risksrequiredarray, 1–200 rowsRisk rows: title, likelihood (1–5), impact (1–5), treatment (mitigate|transfer|avoid|accept), nextReviewDate (YYYY-MM-DD), isFraudRisk, plus optional category, residual pair, owner, mitigationPlan, mitigatingControls[], externalRef (e.g. Vanta "R-25").
dryRunoptionalbooleantrue → full preview with ZERO writes.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "import_risks",    "arguments": {      "risks": [        {          "title": "Unauthorized access to production",          "category": "security",          "likelihood": 3,          "impact": 4,          "treatment": "mitigate",          "mitigationPlan": "MFA + least privilege",          "residualLikelihood": 2,          "residualImpact": 3,          "nextReviewDate": "2027-06-05",          "isFraudRisk": false,          "externalRef": "R-25"        }      ]    }  }}

Result

json
{  "importBatchId": "<batch-uuid>",  "dryRun": false,  "summary": "Imported: 1 created, 0 enriched, 0 skipped, 0 failed",  "created": [    {      "id": "risk-imp-<sha16>",      "name": "Unauthorized access to production"    }  ],  "updated": [],  "skipped": [],  "errors": [],  "warnings": []}

import_test_runs

Scope
tests:write

Bulk-import HISTORIC automated-test runs from a GRC-platform export (e.g. Vanta's per-test '*-testRun.csv' files), max 100 per call. Each run carries the ORIGINAL collection timestamp (past only). A SOC 2 Type 2 auditor samples runs across the whole observation period by date, and PCI counts quarterly scan cadence, so the source platform's timestamps are the evidence. Match source test names to Strac testIds via list_tests view:'catalog'. Source risk acceptances are preserved verbatim with the ORIGINAL approver. Imported runs appear in each test's run history (labeled by source + original date) and in audit binders. They NEVER count toward current completion %. Idempotent (dedupes on testId + executedAt); dryRun previews; per-row errors[]. Requires admin/owner.

Ask your AI client: “Import the historic test runs from my Vanta Tests export so our auditor can see the full period.”

Input

FieldTypeDescription
sourcerequired'vanta' | 'drata' | 'secureframe' | 'other'The platform the export came from.
runsrequiredarray, 1–100 rowsRun rows: testId (Strac canonical), executedAt (ISO 8601, past only), status (passing|failing|not_applicable), plus optional summary, sourceTestName, findingsTotal, findings[] (≤100, sampled), riskAcceptances[] (original approver/reason/dates, verbatim), affectedResources[].
dryRunoptionalbooleantrue → full preview with ZERO writes.
aiReasoningoptionalstring, 1–2000Optional rationale on the audit-log row.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "import_test_runs",    "arguments": {      "source": "vanta",      "runs": [        {          "testId": "aws-s3-encryption",          "executedAt": "2026-05-20T00:00:00.000Z",          "status": "failing",          "sourceTestName": "S3 buckets allow only HTTPS traffic (AWS)",          "summary": "19 of 42 S3 buckets failing HTTPS-only at export",          "findingsTotal": 19        }      ]    }  }}

Result

json
{  "importBatchId": "<batch-uuid>",  "dryRun": false,  "source": "vanta",  "summary": "Imported: 1 created, 0 enriched, 0 skipped, 0 failed",  "created": [    {      "id": "test-imp-<sha16>",      "name": "S3 buckets allow only HTTPS traffic (AWS)"    }  ],  "updated": [],  "skipped": [],  "errors": [],  "warnings": []}

list_vendors

Scope
vendors:read

List your organization's managed vendors: name, inherent risk, owners, category, review dates, compliance attestations, and import provenance. For a vendor with a PCI DSS Attestation of Compliance on file, pciAttestation says which document counts for PCI DSS 12.8.4, who decided it (filed as pci_aoc, read by Strac from the PDF, or its name: not read, unreadable, or read without confirming the form), the assessed entity, the report date and the review due date (report date + 12 months; an AOC states no expiry). Optional status filter (active/archived/pending/inactive). Pairs with import_vendors for post-import verification.

Ask your AI client: “What vendors do we have, and who owns each one?”

Input

FieldTypeDescription
statusoptional'active' | 'archived' | 'pending' | 'inactive'Filter by vendor lifecycle status. Omit to list all.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_vendors",    "arguments": {}  }}

Result

json
{  "vendors": [    {      "id": "vendor-imp-<sha16>",      "name": "Stripe",      "website": "stripe.com",      "status": "active",      "inherentRisk": "high",      "category": "Payments",      "securityOwner": "security@example.com",      "businessOwner": null,      "businessPurpose": null,      "dataAccess": null,      "compliance": [        "SOC 2"      ],      "securityReviewStatus": "up_to_date",      "lastReviewedAt": "2025-09-23",      "nextReviewDueAt": "2026-09-23T00:00:00.000Z",      "source": "manual",      "importBatchId": "<batch-uuid>"    }  ],  "count": 1}

list_risks

Scope
risks:read

List your organization's risk register: title, category, inherent/residual scores + levels (5×5 bands), treatment, owner, review dates, fraud-risk flag, import provenance, and maintenance provenance (maintainedBy: 'strac-ai' | 'vendor-sync' | 'manual'; basis; basisKind; aiStale). A 'strac-ai' row is kept current by Strac and counts as risk-program evidence only once a person approves the register. Optional status filter (identified/mitigating/accepted/closed).

Ask your AI client: “Show me our open risks and their residual levels.”

Input

FieldTypeDescription
statusoptional'identified' | 'mitigating' | 'accepted' | 'closed'Filter by risk status. Omit to list all.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_risks",    "arguments": {      "status": "mitigating"    }  }}

Result

json
{  "risks": [    {      "id": "risk-imp-<sha16>",      "title": "Unauthorized access to production",      "category": "security",      "likelihood": 3,      "impact": 4,      "inherentRiskScore": 12,      "inherentRiskLevel": "high",      "residualRiskScore": 6,      "residualRiskLevel": "medium",      "treatment": "mitigate",      "status": "mitigating",      "owner": "security@example.com",      "isFraudRisk": false,      "nextReviewDate": "2027-06-05",      "acceptanceApprover": null,      "externalRef": "R-25",      "importBatchId": "<batch-uuid>"    }  ],  "count": 1}

Vendor lifecycle (write)

Run a vendor's deterministic risk assessment (single or bulk fan-out, with a dryRun preview), record a completed CC6.1/CC9.2 security review, and file the documents a vendor hands you (SOC 2 report, PCI AOC, DPA, pen-test summary) against their record. These are the vendor-lifecycle actions from the in-app Vendors page, exposed to agents. Assessments reuse the same engine as the Assess button; a recorded review stamps the vendor up to date and is marked agent-recorded in the binder. For vendor documents, prefer the presigned begin/finalize pair (≤50 MB, bytes straight to S3) over the inline base64 tool.

run_vendor_risk_assessment

Scope
vendors:write

Run the deterministic risk assessment for one managed vendor (the same engine as the in-app Assess button). Returns inherent + residual risk bands (low/medium/high/critical), the 1–25 scores, top drivers, evidence confidence, and a plain-English rationale. The engine auto-loads the vendor's own evidence (documents, reviews, issues, integrations). You don't pass evidence in. Set dryRun: true to preview the band without writing; otherwise it records a point-in-time assessment and updates the vendor's displayed risk. Resolve vendorId with list_vendors. Requires admin/owner.

Ask your AI client: “Assess Stripe's vendor risk.”

Input

FieldTypeDescription
vendorIdrequiredstringThe managed vendor to assess (resolve with list_vendors).
dryRunoptionalbooleantrue → compute and return the band with ZERO writes.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "run_vendor_risk_assessment",    "arguments": {      "vendorId": "vendor-imp-<sha16>"    }  }}

Result

json
{  "dryRun": false,  "persisted": true,  "vendorId": "vendor-imp-<sha16>",  "vendorName": "Stripe",  "assessmentId": "risk-assessment-<uuid>",  "inherent": {    "band": "high",    "score": 16,    "impact": 4,    "likelihood": 4  },  "residual": {    "band": "medium",    "score": 9,    "impact": 3,    "likelihood": 3  },  "evidenceConfidence": "medium",  "mirroredInherentRisk": "medium",  "rationale": "Residual risk is medium (impact 3 × likelihood 3 = 9/25)…"}

run_vendor_risk_assessments

Scope
vendors:write

Run the deterministic risk assessment for MULTIPLE managed vendors in one call (the fan-out version of run_vendor_risk_assessment). Pass up to 25 vendorIds; chunk larger sets across calls. Returns a per-vendor result plus a per-vendor errors[]. A bad id never fails the others. Set dryRun: true to preview every band with ZERO writes. There is no bulk security-review. Review findings are vendor-specific, so use record_vendor_security_review per vendor. Requires admin/owner.

Ask your AI client: “Assess all my high-risk vendors. Preview first.”

Input

FieldTypeDescription
vendorIdsrequiredarray, 1–25Managed vendor ids to assess (resolve with list_vendors). Duplicates are assessed once.
dryRunoptionalbooleantrue → compute every band with ZERO writes.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "run_vendor_risk_assessments",    "arguments": {      "vendorIds": [        "vendor-imp-<sha16-a>",        "vendor-imp-<sha16-b>"      ],      "dryRun": true    }  }}

Result

json
{  "dryRun": true,  "summary": "DRY RUN, would assess: 2 ok, 0 failed of 2",  "assessed": [    {      "vendorId": "vendor-imp-<sha16-a>",      "vendorName": "Stripe",      "inherentBand": "high",      "residualBand": "medium",      "residualScore": 9,      "evidenceConfidence": "medium",      "assessmentId": null    }  ],  "errors": []}

record_vendor_security_review

Scope
vendors:write

Record a COMPLETED security review for one managed vendor (the CC6.1/CC9.2 attestation auditors expect). Marks the vendor's security review up to date and computes the next review due date. summaryOfFindings is required (10–2000 chars). This is the audit evidence of what was reviewed, since files can't be uploaded over MCP; optionally pin existing vendor documents with reviewedDocIds. The review actor is taken from your token; reviewer is an optional display-only name. Recording the same vendor + outcome + findings twice is idempotent. Requires admin/owner.

Ask your AI client: “Record a passed security review for Stripe. I reviewed their SOC 2 and DPA.”

Input

FieldTypeDescription
vendorIdrequiredstringThe managed vendor being reviewed (resolve with list_vendors).
outcomerequired'passed' | 'passed_with_exceptions' | 'failed'The review verdict.
summaryOfFindingsrequiredstring, 10–2000What was checked and found (the CC9.2 evidence of record).
reviewedDocIdsoptionalarray, ≤50Ids of this vendor's existing documents the reviewer attests to having reviewed. Ids not belonging to this vendor are dropped.
revieweroptionalstringDisplay-only name of the human reviewer, if different from the caller. Does not change the audit actor.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "record_vendor_security_review",    "arguments": {      "vendorId": "vendor-imp-<sha16>",      "outcome": "passed",      "summaryOfFindings": "Reviewed SOC 2 report and DPA; no exceptions found."    }  }}

Result

json
{  "reviewId": "review-mcp-<sha16>",  "vendorId": "vendor-imp-<sha16>",  "status": "complete",  "outcome": "passed",  "completedAt": "2026-07-14T18:56:43.620Z",  "reviewer": "you@example.com",  "recordedBy": "you@example.com",  "evidenceChannel": "summary",  "reviewedEvidenceIds": [],  "droppedDocIds": [],  "idempotentReplay": false,  "vendorSecurityReviewStatus": "up_to_date",  "securityReviewDate": "2026-07-14T18:56:43.620Z",  "nextReviewDueAt": "2027-07-14T00:00:00.000Z"}

attach_vendor_document

Scope
vendors:write

Upload a document FROM a vendor (their SOC 2 report, DPA, pen-test summary, security questionnaire…) and attach it to one managed vendor. This is the third-party evidence auditors expect for CC6.1/CC9.2. Accepts inline base64 content, max 4 MB after decode. For a PCI DSS Attestation of Compliance use 'pci_aoc'. PREFER begin_vendor_document_upload + finalize_vendor_document_upload for any real file. The presigned handshake streams the bytes straight to S3 (≤50 MB, no base64-through-the-model). Resolve vendorId with list_vendors. Requires admin/owner.

Ask your AI client: “Attach this small DPA PDF to Example CRM's vendor record.”

Input

FieldTypeDescription
vendorIdrequiredstring, 1–200The managed vendor to attach the document to (resolve with list_vendors).
docTyperequireddocType enumOne of soc2_report, iso27001_cert, pci_aoc, dpa, baa, msa, sla, security_questionnaire, privacy_policy, security_whitepaper, pentest_summary, subprocessor_list, other. Use pci_aoc for a PCI DSS Attestation of Compliance; Strac reads the PDF to record which company and services it covers and its report date.
namerequiredstring, 1–200Human-readable document name, e.g. 'Example CRM SOC 2 Type II 2026'.
contentTyperequiredMIME enumMIME type of the file (pdf, docx, doc, xlsx, xls, pptx, png, jpeg, gif, webp, plain, csv, json, zip). Other types are rejected.
contentBase64requiredstring (base64)Base64-encoded file content. Max 4 MB after decode (~5.3 MB base64).
expiresAtoptionalstring, 1–40Optional expiry date (ISO 8601), e.g. the SOC 2 report period end or the attestation expiry.
notesoptionalstring, 0–2000Optional free-text notes.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "attach_vendor_document",    "arguments": {      "vendorId": "vendor-demo1",      "docType": "dpa",      "name": "Example CRM DPA 2026",      "contentType": "application/pdf",      "contentBase64": "<base64-bytes>"    }  }}

Result

json
{  "documentId": "doc-<uuid>",  "vendorId": "vendor-demo1",  "docType": "dpa",  "name": "Example CRM DPA 2026",  "status": "current",  "s3Key": "vendor-documents/comp_demo/vendor-demo1/<uuid>-Example_CRM_DPA_2026.pdf",  "fileSize": 248192,  "fileType": "application/pdf",  "sha256": "<sha256>",  "uploadedAt": "2026-07-16T18:56:43.620Z",  "uploadedBy": "you@example.com"}

begin_vendor_document_upload

Scope
vendors:write

Step 1 of the presigned vendor-document handshake, the PREFERRED way to attach a real file FROM a vendor (SOC 2 report, PCI AOC, DPA, pen-test summary…) to one managed vendor. Returns a short-lived S3 PUT URL. Upload the raw bytes with curl -T, then call finalize_vendor_document_upload with the returned uploadId. The bytes stream straight to S3 (≤50 MB), never base64-through-the-model. All routing metadata (vendorId, docType, name, expiresAt, notes) is pinned here; finalize takes only the uploadId. Resolve vendorId with list_vendors. Requires admin/owner.

Ask your AI client: “Upload Example CRM's SOC 2 Type II report and attach it to their vendor record.”

Input

FieldTypeDescription
vendorIdrequiredstring, 1–200The managed vendor to attach the document to (resolve with list_vendors). Validated at begin and re-validated at finalize.
docTyperequireddocType enumOne of soc2_report, iso27001_cert, pci_aoc, dpa, baa, msa, sla, security_questionnaire, privacy_policy, security_whitepaper, pentest_summary, subprocessor_list, other. Use pci_aoc for a PCI DSS Attestation of Compliance; Strac reads the PDF to record which company and services it covers and its report date.
namerequiredstring, 1–200Human-readable document name, e.g. 'Example CRM SOC 2 Type II 2026'.
contentTyperequiredMIME enumMIME type of the file; must match the Content-Type sent on the PUT (pdf, docx, doc, xlsx, xls, pptx, png, jpeg, gif, webp, plain, csv, json, zip).
expiresAtoptionalstring, 1–40Optional expiry date (ISO 8601), e.g. the SOC 2 report period end or the attestation expiry.
notesoptionalstring, 0–2000Optional free-text notes.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_vendor_document_upload",    "arguments": {      "vendorId": "vendor-demo1",      "docType": "soc2_report",      "name": "Example CRM SOC 2 Type II 2026",      "contentType": "application/pdf",      "expiresAt": "2027-03-31"    }  }}

Result

json
{  "uploadId": "<upload-id>",  "uploadUrl": "https://<bucket>.s3.amazonaws.com/vendor-documents/comp_demo/vendor-demo1/_staging/<upload-id>/Example_CRM_SOC_2_Type_II_2026.pdf?X-Amz-Signature=<sig>",  "uploadExpiresAt": "2026-07-16T19:10:00.000Z",  "maxBytes": 52428800,  "contentType": "application/pdf",  "next": "Upload the file bytes directly to S3, then finalize:\n  1. curl -X PUT -T <path-to-file> -H 'Content-Type: …' '<uploadUrl>'\n  2. call finalize_vendor_document_upload with this uploadId"}

finalize_vendor_document_upload

Scope
vendors:write

Step 2 of the vendor-document handshake. After you PUT the file bytes to the URL from begin_vendor_document_upload, pass the uploadId; the server reads the uploaded object (≤50 MB), version-pinned-copies it to the vendor's final key, and stores it as the vendor's current evidence on its Documents tab. Idempotent: a repeated call with the same uploadId returns the same result. Returns precondition_failed if the file was not uploaded first. Requires admin/owner.

Ask your AI client: “Finalize the vendor document upload. Here is the uploadId.”

Input

FieldTypeDescription
uploadIdrequiredstring, 1–80The uploadId returned by begin_vendor_document_upload.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "finalize_vendor_document_upload",    "arguments": {      "uploadId": "<upload-id>"    }  }}

Result

json
{  "documentId": "doc-<uuid>",  "vendorId": "vendor-demo1",  "docType": "soc2_report",  "name": "Example CRM SOC 2 Type II 2026",  "status": "current",  "s3Key": "vendor-documents/comp_demo/vendor-demo1/<uuid>-Example_CRM_SOC_2_Type_II_2026.pdf",  "fileSize": 4823040,  "fileType": "application/pdf",  "sha256": "<sha256>",  "uploadedAt": "2026-07-16T18:56:43.620Z",  "uploadedBy": "you@example.com",  "s3VersionId": "<s3-version-id>"}

Integration handoff

See what is already connected, then drive a browser-based OAuth handoff to connect GWS / AWS / Slack and poll to completion.

list_integrations

Scope
compliance:read

The tenant's connected integrations, so a client never re-suggests one that exists. `isLive` means a real connection (anything except mid-setup pending or removed/disconnected); `mcpConnectable`/`mcpAlias` mark types connect_integration can wire up over MCP. Each row carries its `integrationId` and, when stored, what the Integrations page asks you to fix: `missingPermissions` (scopes the connection was not granted) and `grantedScopes`; for Tailscale the connection type (`authType`) and, for an API access token, its expiry (`apiTokenExpiry`) and why Tailscale stopped accepting it (`apiTokenDriftReason`); for AWS the `accountId` and, on an account connected through an AWS Organization, its management account (`organization`). A live row with missingPermissions, a drift reason or status drifted/error needs a reconnect. Credentials are never returned. Echoes the bound organizationId/organizationName so a multi-tenant client can confirm which org the token resolves to.

Ask your AI client: “Which integrations do we already have connected?”

No arguments.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_integrations",    "arguments": {}  }}

Result

json
{  "organizationId": "comp_demo",  "organizationName": "Acme Inc",  "integrations": [    {      "integrationId": "<integration-uuid>",      "type": "google-workspace",      "label": "Google Workspace",      "status": "active",      "isLive": true,      "mcpConnectable": true,      "mcpAlias": "gws",      "connectedAt": "2026-03-01T00:00:00.000Z"    },    {      "integrationId": "<integration-uuid>",      "type": "tailscale",      "label": "tailscale",      "status": "active",      "isLive": true,      "mcpConnectable": false,      "connectedAt": "2026-09-01T00:00:00.000Z",      "authType": "api-token",      "apiTokenExpiry": {        "at": "2026-11-30T00:00:00.000Z",        "basis": "at-latest"      }    }  ],  "count": 2,  "liveTypes": [    "google-workspace",    "tailscale"  ],  "connectableViaMcp": [    "gws",    "aws",    "slack"  ],  "note": "Do not suggest connecting a type with isLive:true (it is already connected); a live row with missingPermissions, apiTokenDriftReason, or status drifted/error needs a reconnect at comply.strac.io/integrations."}

connect_integration

Scope
compliance:read

Returns a connect URL the user opens in a browser to authorize GWS / AWS / Slack. Poll get_connection_status until it flips. 30-minute window.

Ask your AI client: “Connect our Google Workspace.”

Input

FieldTypeDescription
integrationTyperequiredstringWhich integration to connect.gwsawsslack

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "connect_integration",    "arguments": {      "integrationType": "gws"    }  }}

Result

json
{  "connectionId": "<conn-uuid>",  "connectUrl": "https://comply.strac.io/integrations?connect=gws&mcp_connection_id=<conn-uuid>",  "integrationType": "gws",  "integrationLabel": "Google Workspace",  "expiresAt": "2026-05-27T16:30:00.000Z",  "state": "pending",  "pollWith": "get_connection_status({ connectionId: \"<conn-uuid>\" })"}

get_connection_status

Scope
compliance:read

Polls a pending connection. Returns connected (with integrationId), pending, timeout, or not_found.

Ask your AI client: “Did the Google Workspace connection finish?”

Input

FieldTypeDescription
connectionIdrequiredstring, 1–120The connectionId from connect_integration.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_connection_status",    "arguments": {      "connectionId": "<conn-uuid>"    }  }}

Result

json
{  "connectionId": "<conn-uuid>",  "state": "connected",  "integrationType": "gws",  "integrationId": "gws-acme",  "createdAt": "2026-05-27T16:00:00.000Z",  "expiresAt": "2026-05-27T16:30:00.000Z",  "message": "Google Workspace connected."}

Auditor replies

Respond to an auditor's evidence requests entirely over MCP (no dashboard required). List/read requests, then prepare a DRAFT reply from existing binder evidence, a newly uploaded file, or a text note, and submit it to send. Prepare and submit are separate steps so the auditor is never emailed by accident.

list_audit_requests

Scope
audits:read

List the evidence requests an auditor has raised on an audit (what they're waiting on), with response/draft counts and who created each.

Ask your AI client: “What is the auditor waiting on for the 2026 SOC 2 audit?”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id from list_audits.
statusoptionalstringRequest status filter.requestedsubmittedacceptedrejectednarequest-moreallDefault: all

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "list_audit_requests",    "arguments": {      "auditId": "<audit-uuid>",      "status": "requested"    }  }}

Result

json
{  "auditId": "<audit-uuid>",  "requests": [    {      "requestId": "<request-uuid>",      "controlId": "cc1.1",      "title": "Provide Q1 access review",      "status": "requested",      "evidenceType": "manual",      "createdByKind": "auditor",      "dueDate": null,      "responseCount": 0,      "draftCount": 0,      "auditorAttachmentCount": 1,      "createdAt": "2026-05-22T00:00:00.000Z"    }  ],  "count": 1}

get_audit_request

Scope
audits:read

One auditor request in full, plus attachableEvidenceRefs: the binder evidence already mapped to its control (the valid evidenceRefIds for prepare_audit_request_response).

Ask your AI client: “What evidence can I attach to answer this auditor request?”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id.
requestIdrequiredstring, 1–200Request id from list_audit_requests.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_audit_request",    "arguments": {      "auditId": "<audit-uuid>",      "requestId": "<request-uuid>"    }  }}

Result

json
{  "auditId": "<audit-uuid>",  "requestId": "<request-uuid>",  "controlId": "cc1.1",  "title": "Provide Q1 access review",  "status": "requested",  "attachableEvidenceRefs": [    {      "refId": "<ref-hash>",      "type": "policy",      "sourceId": "POL-SEC-001",      "versionId": "v3",      "title": "Access Control Policy",      "capturedAt": "2026-04-01T00:00:00.000Z"    }  ],  "responses": [],  "auditorAttachments": []}

get_audit_request_attachment_download_url

Scope
audits:read

Short-lived (1h) download URL for one of the auditor's own attachments on a request (attachmentId from get_audit_request).

Ask your AI client: “Download the example the auditor attached to this request.”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id.
requestIdrequiredstring, 1–200Request id.
attachmentIdrequiredstring, 1–120Attachment id from get_audit_request.auditorAttachments.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "get_audit_request_attachment_download_url",    "arguments": {      "auditId": "<audit-uuid>",      "requestId": "<request-uuid>",      "attachmentId": "<attachment-uuid>"    }  }}

Result

json
{  "downloadUrl": "https://s3.amazonaws.com/...signed...",  "fileName": "sample-access-review.xlsx",  "contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",  "expiresInSeconds": 3600}

begin_audit_response_upload

Scope
audit-requests:write

Begin a presigned upload for a NEW file to attach to an auditor request: returns { uploadId, uploadUrl }. PUT the bytes to uploadUrl, then pass uploadId to prepare_audit_request_response. For evidence already in the binder, use evidenceRefIds instead.

Ask your AI client: “I have a fresh access-review PDF to attach to this request.”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id.
requestIdrequiredstring, 1–200Request id.
fileNamerequiredstring, 1–255Original filename.
fileSizerequirednumber (≤ 50MB)File size in bytes.
contentTyperequiredstringMIME type (PDF/Office/CSV/PNG/JPEG/ZIP/text).

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "begin_audit_response_upload",    "arguments": {      "auditId": "<audit-uuid>",      "requestId": "<request-uuid>",      "fileName": "q1-access-review.pdf",      "fileSize": 524288,      "contentType": "application/pdf"    }  }}

Result

json
{  "uploadId": "<upload-uuid>",  "uploadUrl": "https://s3.amazonaws.com/...signed-put...",  "expiresInSeconds": 600,  "next": "PUT the file bytes to uploadUrl, then call prepare_audit_request_response with uploadId=\"<upload-uuid>\"."}

prepare_audit_request_response

Scope
audit-requests:write

Prepare a DRAFT response to an auditor request. Does NOT send. Answer with existing binder evidence (evidenceRefIds from get_audit_request), a newly uploaded file (uploadId), and/or a text note. At least one is required. Idempotent. Submit it with submit_audit_request_response (or approve in the dashboard).

Ask your AI client: “Draft a reply to this request using the access policy already in our binder.”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id.
requestIdrequiredstring, 1–200Request id.
messageoptionalstring, 1–4000Optional note to the auditor.
evidenceRefIdsoptionalstring[] (≤ 50)refIds from get_audit_request.attachableEvidenceRefs.
uploadIdoptionalstring, 1–120An uploadId from begin_audit_response_upload (after the PUT).
idempotencyKeyoptionalstring, 1–120Usually omit. Idempotency is content-derived.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "prepare_audit_request_response",    "arguments": {      "auditId": "<audit-uuid>",      "requestId": "<request-uuid>",      "message": "Q1 access review attached.",      "evidenceRefIds": [        "<ref-hash>"      ]    }  }}

Result

json
{  "deliveryState": "draft_prepared",  "auditorNotified": false,  "awaitingHumanApproval": true,  "requestStatusAfter": "requested",  "responseId": "d-<sha16>",  "evidenceRefsAttached": 1,  "fileAttached": false,  "idempotentReplay": false,  "humanNextStep": "A teammate approves this in Strac → Audits → this audit → Requests → Submit to auditor. The auditor is NOT notified until then.",  "dashboardApprovalUrl": "/compliance/audits/<audit-uuid>?tab=requests"}

submit_audit_request_response

Scope
audit-requests:write

SEND a prepared draft response to the auditor (the deliberate approve step). Flips the draft to submitted, sets the request status to submitted, and notifies the auditor. Irreversible. Idempotent: re-submitting is a no-op.

Ask your AI client: “Send the draft reply to the auditor.”

Input

FieldTypeDescription
auditIdrequiredstring, 1–120Audit id.
requestIdrequiredstring, 1–200Request id.
responseIdrequiredstring, 1–120The draft responseId from prepare_audit_request_response.

tools/call request

json
{  "jsonrpc": "2.0",  "id": 1,  "method": "tools/call",  "params": {    "name": "submit_audit_request_response",    "arguments": {      "auditId": "<audit-uuid>",      "requestId": "<request-uuid>",      "responseId": "d-<sha16>"    }  }}

Result

json
{  "deliveryState": "submitted",  "auditorNotified": true,  "alreadySubmitted": false,  "requestStatusAfter": "submitted",  "responseId": "d-<sha16>",  "message": "Submitted to the auditor. The request is now marked submitted and the auditor was notified."}

Tool error envelope

tools/call error results carry a discriminator on _meta["strac.io/errorCode"] so your client can branch on a code, not free text. It is always present on errors, and the message is capped at 500 characters.

jsonerror result
{  "jsonrpc": "2.0",  "id": 1,  "result": {    "content": [{ "type": "text", "text": "test result 'abc-123' not found" }],    "_meta": { "strac.io/errorCode": "not_found" },    "isError": true  }}
errorCodeRecover by
not_foundThe id does not exist, or belongs to another company. Call the matching list_* tool and try a real id.
invalid_argumentThe arguments match the schema but the values do not make sense together. Check the tool's input schema.
precondition_failedThe record exists but is in a state that blocks this action. Change its state first, then retry.
unauthorizedYour token has the scope, but your role in the company does not allow this action. Use a token from an owner or admin.
unknownThe tool refused the call without a category. Read the message for the reason.
internalSomething failed on our side. Retry, and contact support if it keeps happening. The message is generic on purpose.

How your data stays safe

Safe to build against

The server decides who is calling from the token. A userId or createdBy sent in the arguments is rejected. Every read stays inside your company. Asking for another company's data returns not_found, never the data. Every write is recorded in an append-only audit log. Evidence you attach to an audit is pinned to the exact version you attached.

See also