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:
claude mcp add --transport http --scope user strac-comply \ https://mcp.comply.strac.io/mcp- Run the command above.
--scope usermakes the server available in every Claude Code session, not just the current folder. - Start (or restart) Claude Code. MCP config is read at launch.
- Run
/mcp→ selectstrac-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:
codex mcp add strac-comply --url \ https://mcp.comply.strac.io/mcpIf 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
gemini mcp add --scope user --transport http strac-comply \ https://mcp.comply.strac.io/mcpGemini opens your browser to sign in. If it does not, run /mcp auth strac-comply in a session.
Grok Build
grok mcp add --transport http strac-comply \ https://mcp.comply.strac.io/mcpGrok opens your browser to sign in the first time you use it.
Cursor
{ "mcpServers": { "strac-comply": { "url": "https://mcp.comply.strac.io/mcp" } }}Save it, then sign in from Cursor Settings > MCP.
OpenCode
{ "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.
| Scope | What 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:writepolicies:approvedocuments:writeevidence:writevendors:writerisks: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.
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
ScopeReturns 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
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_frameworks", "arguments": {} }}Result
{ "organizationName": "Acme Inc", "frameworks": [ { "frameworkId": "soc2", "label": "SOC 2" }, { "frameworkId": "iso27001", "label": "ISO 27001:2022" } ], "count": 2}get_compliance_status
ScopeAggregate 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
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_compliance_status", "arguments": {} }}Result
{ "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
ScopeCursor-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
| Field | Type | Description |
|---|---|---|
| frameworkIdoptional | string, 1–80 | Filter to one framework. |
| limitoptional | integer 1–200 | Page size.Default: 50 |
| cursoroptional | string | Opaque pagination cursor from a prior nextCursor. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_controls", "arguments": { "frameworkId": "soc2", "limit": 50 } }}Result
{ "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
ScopeA 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
| Field | Type | Description |
|---|---|---|
| controlIdrequired | string, 1–120 | Control id, e.g. CC6.1. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_control", "arguments": { "controlId": "CC6.1" } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| frameworkIdoptional | string | Optional framework to restrict to, e.g. SOC2. Omit for all active frameworks. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_next_actions", "arguments": { "frameworkId": "SOC2" } }}Result
{ "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
ScopePolicies 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
| Field | Type | Description |
|---|---|---|
| frameworkIdoptional | string, 1–80 | Filter by framework. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_policies", "arguments": {} }}Result
{ "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
ScopeA 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
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | Policy id, e.g. POL-SEC-001. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_policy", "arguments": { "policyId": "POL-SEC-001" } }}Result
{ "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
ScopeCatalog 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
| Field | Type | Description |
|---|---|---|
| sourceoptional | string | Which documents to include.catalogadhocallDefault: all |
| frameworkIdoptional | string, 1–80 | Filter catalog docs by framework. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_documents", "arguments": { "source": "all" } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| statusoptional | string | Employment status filter (terminated also returns suspended accounts).activeterminatedallDefault: active |
| departmentoptional | string, 1–120 | Case-sensitive department match. |
| includeServiceAccountsoptional | boolean | Include accounts marked as service accounts.Default: true |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_employees", "arguments": { "status": "active", "department": "Engineering" } }}Result
{ "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
ScopeOnboarding/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
| Field | Type | Description |
|---|---|---|
| typeoptional | string | Checklist-type filter.onboardingoffboardingallDefault: all |
| statusoptional | string | Status filter.activecompletedvoidedallDefault: all |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_lifecycle_checklists", "arguments": { "type": "offboarding", "status": "active" } }}Result
{ "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
ScopeOne 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
| Field | Type | Description |
|---|---|---|
| checklistIdrequired | string, 1–200 | The episodeId from list_lifecycle_checklists. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_lifecycle_checklist", "arguments": { "checklistId": "ep_demo1" } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| categoryoptional | string | Filter returned rows to one category; org-wide counts are unaffected.computedata-storescode-registriesnetwork-edgesecurity-infrasaas-applicationsdevicesother |
| includeScopedOutoptional | boolean | Include resources the admin marked out of scope (with their reason).Default: false |
| limitoptional | integer 1–500 | Max rows returned.Default: 100 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_inventory", "arguments": { "category": "data-stores", "limit": 100 } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| statusoptional | string | Account status filter.activeinvitedsuspendeddeactivatedallDefault: all |
| mfaEnrolledoptional | boolean | Filter by MFA enrollment; accounts whose system does not report MFA are excluded by either value. |
| systemoptional | string, 1–80 | One system by name, case-insensitive substring. |
| noOwneroptional | boolean | Only accounts nobody owns (ignored when ownership could not be measured). |
| serviceAccountoptional | boolean | Only service accounts (true) or only the rest (false). |
| emailoptional | string, 1–254 | Only accounts for this address. |
| limitoptional | integer 1–500 | Cap results at N.Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_saas_accounts", "arguments": { "status": "active", "mfaEnrolled": false } }}Result
{ "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
ScopePersisted 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
| Field | Type | Description |
|---|---|---|
| stateoptional | string | Triage-state filter.needs_reviewignoredrejectedallDefault: all |
| limitoptional | integer 1–500 | Cap results at N.Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_saas_apps", "arguments": { "state": "needs_review" } }}Result
{ "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
ScopeAudits with lifecycle status and pending-request counts.
Ask your AI client: “What audits are in flight and how many requests are pending?”
Input
| Field | Type | Description |
|---|---|---|
| statusoptional | string | Lifecycle filter.planningin-flightcompleteallDefault: all |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_audits", "arguments": { "status": "in-flight" } }}Result
{ "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
ScopeA single audit with sampled-control count, evidence-request breakdown, and findings rollup.
Ask your AI client: “Summarize the 2026 SOC 2 audit binder.”
Input
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id (UUID). |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_audit", "arguments": { "auditId": "<audit-uuid>" } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | The auditId from list_audits. |
| includeNotReviewedoptional | boolean | Include controls the auditor has not yet reviewed.Default: false |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_audit_verdicts", "arguments": { "auditId": "<audit-uuid>" } }}Result
{ "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
ScopeAutomated 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
| Field | Type | Description |
|---|---|---|
| statusoptional | string | Result status filter.passingfailingskippederrorallDefault: all |
| severityoptional | string | Severity filter.criticalhighmediumlowinfoallDefault: all |
| integrationIdoptional | string, 1–120 | Filter to one integration, e.g. aws-123456789012. |
| limitoptional | integer 1–500 | Max results.Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_tests", "arguments": { "status": "failing", "severity": "high" } }}Result
{ "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
ScopeOne 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
| Field | Type | Description |
|---|---|---|
| testResultIdrequired | string, 1–200 | Test result id. |
| includeScreenshotDownloadUrloptional | boolean | Mint a short-lived (5 min) presigned download link for a ready console screenshot.Default: false |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_test_result", "arguments": { "testResultId": "<result-uuid>" } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| classoptional | string | Only this class.risk-acceptedscope-excludedalternative-controlmonitoring-pausedpending-decision |
| statusoptional | string | Only this status ('expiring' = ends within 30 days).activeexpiringexpiredpendingrevoked |
| kindoptional | string | Only waivers recorded in this place in the product (e.g. control-not-applicable). |
| noEndDateoptional | boolean | Only in-force waivers with no end date. |
| limitoptional | integer 1–500 | Cap results at N.Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_waivers", "arguments": { "noEndDate": true } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| sourceoptional | string | One source ('github' = Dependabot + code scanning + secret scanning).dependabotcode-scanningsecret-scanningaws-inspectoraws-security-hubaws-ssmcrowdstrikesupabasejamfgithub |
| severityoptional | string | Only this severity.criticalhighmediumlow |
| knownExploitedoptional | boolean | Only findings on the CISA known-exploited list. |
| pastSlaoptional | boolean | Only findings past their remediation deadline. |
| limitoptional | integer 1–500 | Cap results at N.Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_vulnerabilities", "arguments": { "severity": "critical", "pastSla": true } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| reviewIdoptional | string, 1–80 | A review's id; returns that review with its frozen lines (up to 500 accounts and 500 grants). |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_access_reviews", "arguments": {} }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| statusoptional | string | Filter by status.pendingapproveddeniedprovisionedrecordedallDefault: all |
| limitoptional | integer 1–500 | Cap results at N (newest first).Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_access_requests", "arguments": { "status": "pending" } }}Result
{ "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
ScopeLists 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
| Field | Type | Description |
|---|---|---|
| integrationTypeoptional | string | Only this integration. |
| resourceKindoptional | string | Only this kind. |
| limitoptional | integer, 1–500 | Cap results.Default: 200 |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_scope_exclusions", "arguments": { "integrationType": "aws" } }}Result
{ "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
ScopeReads 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
| Field | Type | Description |
|---|---|---|
| includeMembersoptional | boolean | Owner/admin: also list active workspace members (the addresses alsoNotify accepts).Default: false |
| includeSlackChannelsoptional | boolean | Owner/admin: also list the Slack channels a summary can be posted to (ids for slackChannelId).Default: false |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_vulnerability_reminder_settings", "arguments": {} }}Result
{ "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
ScopeReads 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
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_completion_alert_settings", "arguments": {} }}Result
{ "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
ScopeRe-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
| Field | Type | Description |
|---|---|---|
| testIdoptional | string, 1–200 | Test-definition id to re-run (e.g. 'gws-mfa-enabled'). One of testId / testResultId is required. |
| testResultIdoptional | string, 1–200 | Alternatively, the result-row id (from list_tests / get_test_result); its testId is resolved server-side. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "rerun_test", "arguments": { "testId": "aws-s3-encryption" } }}Result
{ "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
ScopePolls 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
| Field | Type | Description |
|---|---|---|
| executionIdrequired | string, 1–200 | The executionId returned by rerun_test. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_test_execution", "arguments": { "executionId": "<execution-id>" } }}Result
{ "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
ScopePROPOSES 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
| Field | Type | Description |
|---|---|---|
| testIdrequired | string, 1–200 | The failing test (from list_tests / get_next_actions). Must be accept-risk-eligible. |
| resourceIdoptional | string, 1–400 | The flagged resource to accept (from the test finding). One of resourceId / findingId is required unless wholeTest is set. |
| findingIdoptional | string, 1–200 | Alternatively, the finding id (resolved + tenant-checked server-side). |
| rationalerequired | string, 10–2000 | Why this risk is acceptable. Goes verbatim into the audit binder for the auditor. |
| affectedControlsoptional | string[] (≤50) | Optional control ids this waiver touches. Display-only context for the human approver. |
| proposedReviewByoptional | string, 1–40 | Optional re-review date (YYYY-MM-DD).Default: +12 months |
| wholeTestoptional | true | Waive 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
{ "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
{ "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
ScopeDoes 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
| Field | Type | Description |
|---|---|---|
| inventoryKeyoptional | string | An 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. |
| integrationTypeoptional | string | The integration the resource belongs to (aws, github, kandji, …). From scopeOut.integrationType. Required unless inventoryKey is given. |
| resourceKindoptional | string | The kind (s3-bucket, repository, environment, device, …). From scopeOut.resourceKind. Required unless inventoryKey is given. |
| resourceIdoptional | string, 1–256 | The provider's immutable id of ONE resource. One of resourceId / resourceIds is required. |
| resourceIdsoptional | string[] (≤200) | Several resources of the same kind. Requires reasonCategory. |
| reasonrequired | string, 10–2000 | Why the resource is out of scope. Shown to auditors on every affected check. |
| reasonCategoryoptional | string | decommissioned, 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. |
| resourceNameoptional | string | Display label for a single resource. The key is always derived from resourceId. |
| aiReasoningoptional | string, 1–2000 | Optional internal reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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
ScopeDoes 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
| Field | Type | Description |
|---|---|---|
| inventoryKeyoptional | string | An excluded Inventory row's key from list_inventory (includeScopedOut: true). Use it instead of integrationType / resourceKind / resourceId. |
| integrationTypeoptional | string | From list_scope_exclusions. Required unless inventoryKey is given. |
| resourceKindoptional | string | From list_scope_exclusions. Required unless inventoryKey is given. |
| resourceIdoptional | string, 1–256 | One excluded resource. One of resourceId / resourceIds is required. |
| resourceIdsoptional | string[] (≤200) | Several excluded resources of the same kind. |
| aiReasoningoptional | string, 1–2000 | Optional internal reasoning for the MCP audit log. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "restore_to_scope", "arguments": { "integrationType": "aws", "resourceKind": "s3-bucket", "resourceId": "acme-legacy-exports" } }}Result
{ "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
ScopeLists 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
| Field | Type | Description |
|---|---|---|
| testResultIdrequired | string, 1–200 | The id of the test result row from list_tests / get_test_result. |
| includeDownloadUrlsoptional | boolean | When true, include a short-lived presigned download URL per evidence file.Default: false |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_test_evidence", "arguments": { "testResultId": "<result-id>" } }}Result
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| testResultIdrequired | string, 1–200 | The id of the test result row from list_tests / get_test_result (the testResultId field, NOT the secondary testId). |
| namerequired | string, 1–256 | Human-readable evidence name, e.g. 'Q1 access review export'. |
| contentTyperequired | string, 1–128 | MIME type of the file (e.g. application/pdf, image/png). Must match the Content-Type sent on the PUT. |
| notesoptional | string, ≤2048 | Optional context for auditors. |
tools/call request
{ "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
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| uploadIdrequired | string, 1–80 | The uploadId returned by begin_test_evidence_upload. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "finalize_test_evidence_upload", "arguments": { "uploadId": "<upload-id>" } }}Result
{ "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
ScopeSets 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
| Field | Type | Description |
|---|---|---|
| testIdsrequired | string[], 1–200 | Check ids from list_tests. |
| ownerrequired | string | null | An active member's email, or null to clear the per-test owner. |
| aiReasoningoptional | string, ≤2000 | Optional reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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
ScopeChanges 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
| Field | Type | Description |
|---|---|---|
| expectedUpdatedAtoptional | string | null | settings.updatedAt from get_vulnerability_reminder_settings. Omit only when nothing is saved yet. |
| enabledoptional | boolean | true 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. |
| dueSoonDaysoptional | integer, 0–30 | Heads-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. |
| alsoNotifyoptional | string[], ≤10 | The full list of extra member addresses. Keep every stored address and add new ones: removing any is refused. |
| slackChannelIdoptional | string | null | A channel id from includeSlackChannels, only when no channel is set. The Strac app must be in the channel. |
| aiReasoningoptional | string, ≤2000 | Optional reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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
ScopeMarks 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
| Field | Type | Description |
|---|---|---|
| controlIdrequired | string, 1–120 | Control id, e.g. CC6.1. |
| reasonrequired | string, 10–500 | Why the control does not apply. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeMarks 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
| Field | Type | Description |
|---|---|---|
| evidenceTyperequired | 'document' | 'policy' | 'integration' | Which kind of slot: 'document' (catalog document), 'policy', or 'integration' (an integration-coverage slot on one control). |
| evidenceIdrequired | string, 1–200 | Catalog document id (from list_documents), policy id (from list_policies), or the integration-coverage item id from the control's required-evidence checklist. |
| controlIdoptional | string, 1–200 | Required when evidenceType is 'integration': the control the slot belongs to (e.g. soc2-cc1.3). Rejected for the other kinds. |
| reasonrequired | string, 10–500 | Customer-visible justification, shown in the N/A badge and the audit binder. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log row (not user-visible). |
tools/call request
{ "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
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| controlIdrequired | string, 1–120 | Canonical control id from list_controls, e.g. soc2-cc6.1. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "unmark_control_na", "arguments": { "controlId": "soc2-cc6.1" } }}Result
{ "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
ScopeThe 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
| Field | Type | Description |
|---|---|---|
| evidenceTyperequired | 'document' | 'policy' | 'integration' | Which kind of slot to restore. |
| evidenceIdrequired | string, 1–200 | Catalog document id, policy id, or the integration-coverage item id. |
| controlIdoptional | string, 1–200 | Required for evidenceType=integration: the control the slot belongs to. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "unmark_evidence_na", "arguments": { "evidenceType": "document", "evidenceId": "DOC-CC6-PHYSICAL-SECURITY" } }}Result
{ "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
ScopeLinks 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
| Field | Type | Description |
|---|---|---|
| controlIdrequired | string, 1–120 | Control id, e.g. CC6.1. |
| evidenceTyperequired | string | Kind of evidence.policydocument |
| referenceIdrequired | string, 1–120 | Policy id or document id to attach. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "attach_evidence", "arguments": { "controlId": "CC6.1", "evidenceType": "policy", "referenceId": "POL-SEC-001" } }}Result
{ "controlId": "CC6.1", "evidenceType": "policy", "referenceId": "POL-SEC-001", "attached": true, "noop": false, "totalControlsLinked": 4}upload_document
ScopeUploads 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
| Field | Type | Description |
|---|---|---|
| titlerequired | string, 1–200 | Document title. |
| descriptionoptional | string, 0–2000 | Optional description. |
| contentTyperequired | MIME enum | One 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. |
| contentBase64required | string (base64) | Base64 bytes, ≤4 MB after decode. |
| controlIdsrequired | string[], 1–20 | control ids stored on the document (e.g. ['soc2-cc6.1']). At least one is required. For completion %, also call attach_evidence per control. |
| frameworksoptional | string[], cap 10 | additional 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). |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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"}add_document_link
ScopeSatisfies a canonical catalog document slot (DOC-*) with a stored https URL + title instead of an uploaded file, for artifacts that live outside Strac and can't be downloaded (a shared Drive file, an internal wiki page). Counts toward completion exactly like an uploaded file but renders as the distinct 'linked' state and is never shown on public surfaces; a later real-file upload supersedes it (file wins), and re-linking the same url is a no-op.
Ask your AI client: “Our incident report lives in Google Drive. Link it into the DOC-CC7-INCIDENT-REPORT slot.”
Input
| Field | Type | Description |
|---|---|---|
| documentIdrequired | string, 1–120 | The canonical catalog slot id (e.g. 'DOC-CC7-INCIDENT-REPORT'). |
| urlrequired | string (https), 1–2048 | The external artifact URL (https only), stored verbatim. May be an internal link, never exposed publicly. |
| titleoptional | string, 1–300 | Human title for the linked artifact. Defaults to the canonical slot title. |
| sourceoptional | string, 1–60 | Coarse provenance label: 'vanta' | 'google-drive' | 'pagerduty' | 'confluence' | 'web'. Drives the 'held externally' copy.Default: web |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add_document_link", "arguments": { "documentId": "DOC-CC7-INCIDENT-REPORT", "url": "https://drive.google.com/file/d/<file-id>/view", "title": "Incident Report. March 2026", "source": "google-drive" } }}Result
{ "documentId": "DOC-CC7-INCIDENT-REPORT", "versionId": "<version-uuid>", "versionNumber": 1, "publishedAt": "2026-06-05T16:00:00.000Z", "publishedBy": "ciso@example.com", "nextReviewDate": "2027-06-05T16:00:00.000Z", "noop": false, "evidenceKind": "external-link", "externalUrl": "https://drive.google.com/file/d/<file-id>/view", "externalTitle": "Incident Report. March 2026", "externalSource": "google-drive"}If the slot is already satisfied by an uploaded file (or already linked to this url), returns { noop: true, reason, documentId, versionId, evidenceKind } without writing.
upload_policy
ScopeCreates 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
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | Canonical policy id, e.g. POL-SEC-001. |
| markdownoptional | string | Policy body as markdown (sanitized). Mutually exclusive with file fields. |
| fileNameoptional | string, 1–200 | File name with extension. Pair with fileContent. |
| fileContentoptional | string (base64), ≤4 MB raw | Base64 file bytes. Pair with fileName. |
| commentsoptional | string, 0–2000 | Version changelog note. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeAdds 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
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | Policy id. |
| basedOnVersionrequired | positive integer | The version counter you edited from (optimistic lock). |
| markdownoptional | string | New body (markdown). Mutually exclusive with file fields. |
| fileNameoptional | string, 1–200 | File name with extension. Pair with fileContent. |
| fileContentoptional | string (base64), ≤4 MB raw | Base64 file bytes. |
| commentsoptional | string, 0–2000 | Version changelog note. |
| forceoptional | boolean | Skip the stale-version check. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeDeterministic (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
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | Policy id. |
| versionIdoptional | string, 1–120 | Specific version; defaults to current/latest. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "compare_policy", "arguments": { "policyId": "POL-SEC-001" } }}Result
{ "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
ScopeApproves 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
| Field | Type | Description |
|---|---|---|
| versionIdrequired | string, 1–120 | The version id to approve. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "approve_policy", "arguments": { "versionId": "<version-uuid>" } }}Result
{ "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
ScopeRecords 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
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | The policy id (list_policies → policyId), e.g. POL-SEC-001. |
| versionIdoptional | string, 1–120 | The approved version you reviewed. Refused if a newer version was approved since. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "record_policy_review", "arguments": { "policyId": "POL-SEC-001", "versionId": "<version-uuid>" } }}Result
{ "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
ScopeOne 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
| Field | Type | Description |
|---|---|---|
| titlerequired | string, 1–200 | Policy title. |
| descriptionoptional | string, 0–2000 | Optional description. |
| categoryoptional | string, 0–80 | Optional category. |
| frameworksoptional | string[] (≤10) | Framework tags. |
| controlIdsoptional | string[] (≤100) | Controls to map; each must exist. |
| markdownoptional | string | Body as markdown. Mutually exclusive with file fields. |
| fileNameoptional | string, 1–200 | File name with extension. Pair with fileContent. |
| fileContentoptional | string (base64), ≤4 MB raw | Base64 file bytes. |
| commentsoptional | string, 0–2000 | Version changelog note. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeSets 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
| Field | Type | Description |
|---|---|---|
| idsrequired | string[], 1–200 | Control ids from list_controls. |
| ownerrequired | string | null | An active member's email, or null to clear the owner. |
| aiReasoningoptional | string, ≤2000 | Optional reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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
ScopeSets 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
| Field | Type | Description |
|---|---|---|
| idsrequired | string[], 1–200 | Policy ids from list_policies. |
| ownerrequired | string | null | An active member's email, or null to clear the owner. |
| aiReasoningoptional | string, ≤2000 | Optional reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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
ScopeSets 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
| Field | Type | Description |
|---|---|---|
| idsrequired | string[], 1–200 | Document ids from list_documents: a catalog slot id (DOC-…) or a custom document id. |
| ownerrequired | string | null | An active member's email, or null to clear the owner. |
| aiReasoningoptional | string, ≤2000 | Optional reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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
ScopeChanges 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
| Field | Type | Description |
|---|---|---|
| expectedUpdatedAtoptional | string | null | settings.updatedAt from get_completion_alert_settings. |
| belowPercentoptional | integer, 0–100 | Alert when a framework crosses below this %. Accepted only when it is 0 (off) now. |
| dropPointsoptional | integer, 1–100 | Alert when a framework falls at least this many points. Only a smaller value is accepted. |
| aiReasoningoptional | string, ≤2000 | Optional reasoning for the MCP audit log. |
tools/call request
{ "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
{ "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."}Evidence-coverage links
One uploaded document satisfying multiple policy slots. suggest_policy_coverage (read, advisory) starts an async scan that proposes links, get_policy_coverage polls it, and set_policy_coverage (write) applies a confirmed one. The covered slot inherits the source policy's approval + review clock.
suggest_policy_coverage
ScopeADVISORY ONLY: present suggestions to the human; only after they confirm, call set_policy_coverage. Starts a BACKGROUND coverage scan of ONE uploaded source document, a canonical policy (default) or a canonical document (slotType:'document', which additionally requires documents:read), to find OTHER canonical slots the same document plausibly satisfies (cross-type included). Returns a jobId + status immediately; poll get_policy_coverage until 'done'. Re-running for the same source is safe and deduped (same jobId, no duplicate scan); a scan that already finished returns its suggestions inline with status 'done'. Writes nothing.
Ask your AI client: “Which other policy slots does our Information Security Policy cover?”
Input
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | The source slot whose uploaded document to read: canonical policy, CUSTOM-POL id, or (with slotType) a DOC-* catalog id. |
| versionIdoptional | string, 1–120 | Specific version to read; defaults to the source's head/latest. |
| slotTypeoptional | 'policy' | 'document' | Kind of the SOURCE slot. 'document' additionally requires the documents:read scope.Default: policy |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "suggest_policy_coverage", "arguments": { "policyId": "POL-SEC-001" } }}Result
{ "jobId": "cov-1a2b3c4d5e6f", "status": "pending", "sourceId": "POL-SEC-001", "versionId": "<version-uuid>", "note": "ENQUEUED: the coverage scan runs in the background. Poll get_policy_coverage with this jobId until status is no longer 'pending'."}If a prior scan of the same source already finished, the result instead comes back inline with status 'done', classifierStatus, and the suggestions array (same shape as get_policy_coverage).
get_policy_coverage
ScopePolls the async coverage scan started by suggest_policy_coverage, using its jobId. While the scan runs, status is 'pending', so poll again shortly; when 'done', read `classifierStatus` ('ok' = ran, empty suggestions = no additional slots; 'error' = render as "suggestions unavailable") and the `suggestions` array; status 'error' means the job itself failed (see `errorReason`). Writes nothing, after the human confirms a slot, call set_policy_coverage.
Ask your AI client: “Check whether that coverage scan on our InfoSec policy finished.”
Input
| Field | Type | Description |
|---|---|---|
| jobIdrequired | string, 1–64 | The jobId returned by suggest_policy_coverage (e.g. 'cov-1a2b3c…'). |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_policy_coverage", "arguments": { "jobId": "cov-1a2b3c4d5e6f" } }}Result
{ "jobId": "cov-1a2b3c4d5e6f", "status": "done", "sourceId": "POL-SEC-001", "versionId": "<version-uuid>", "classifierStatus": "ok", "suggestions": [ { "id": "POL-ISO-002", "type": "policy", "confidence": "high", "rationale": "Section 4 (Access Provisioning) squarely covers the Access Control slot.", "matchedSections": [ "4. Access Provisioning" ] } ], "note": "ADVISORY ONLY. These are AI suggestions, not links. After the human confirms a slot, call set_policy_coverage."}set_policy_coverage
ScopeLink one evidence slot (policy OR catalog document) to be COVERED BY another evidence item (policy or document). Use when a single uploaded document (e.g. a combined Information Security Policy) legitimately satisfies more than one canonical slot, instead of uploading the same file twice. Cross-type is allowed (a document slot covered by a policy, or vice versa). The covered slot inherits the source's approval and review clock. Pass coveredBy: null to unlink. Requires a written rationale that appears in the audit binder. Links are depth-1: a covered item cannot itself cover others. Document slots/sources must be catalog documents and require the documents:write scope.
Ask your AI client: “Our InfoSec policy already covers POL-ISO-002. Link it instead of uploading again.”
Input
| Field | Type | Description |
|---|---|---|
| policyIdrequired | string, 1–120 | The slot being covered: a policy id, or a catalog document id when slotType is "document". |
| slotTypeoptional | 'policy' | 'document' | Evidence type of the slot. Defaults to 'policy'. 'document' requires documents:write. |
| coveredByrequired | string | null | Source policy/document id whose evidence satisfies this slot, or null to unlink. |
| coveredByTypeoptional | 'policy' | 'document' | Source evidence type ('policy' or 'document'). Defaults to 'policy'. |
| rationaleoptional | string, 10–500 | Customer-visible justification. Required when setting; shown to auditors. |
| aiReasoningoptional | string, 1–2000 | Optional internal AI reasoning on the audit-log row. |
| suggestionConfidenceoptional | 'high' | 'medium' | 'low' | When applying a suggest_policy_coverage suggestion, its confidence for this slot. Recorded on the audit row only. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "set_policy_coverage", "arguments": { "policyId": "POL-ISO-002", "coveredBy": "POL-SEC-001", "rationale": "Section 4 of the InfoSec policy defines access provisioning end to end." } }}Result
{ "policyId": "POL-ISO-002", "coveredBy": "POL-SEC-001", "coveredByType": "policy", "noop": false, "affectedControls": [ "soc2-cc6.1" ], "note": "Coverage link applied. The slot now inherits the source policy's approval and review clock."}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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| fileNamerequired | string, ≤200 | File name with extension. Allowed: .docx, .doc, .pdf. Path-traversal-safe (no /, \, .., null bytes). |
| policyIdoptional | string, 1–120 | Existing 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. |
| titleoptional | string, 1–200 | Custom-policy title. Creates a new CUSTOM-POL-* policy. Mutually exclusive with policyId. |
| descriptionoptional | string, 0–2000 | Custom policy: optional description. |
| categoryoptional | string, 0–80 | Custom policy: optional category. Defaults to 'Custom'. |
| frameworksoptional | string[] (≤10) | Custom policy: optional framework codes, e.g. ['soc2']. |
| controlIdsoptional | string[] (≤100) | Custom policy: optional control codes to map; each must exist. |
| basedOnVersionoptional | positive integer | Update 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. |
| forceoptional | boolean | Update mode: skip the stale-version check. Use only after surfacing the conflict; defaulting it loses human edits silently. Requires basedOnVersion. |
| commentsoptional | string, 0–2000 | Optional human-visible changelog comment. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "begin_policy_upload", "arguments": { "policyId": "POL-SEC-001", "fileName": "information-security-policy.docx" } }}Result
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| uploadIdrequired | string, 1–80 | The uploadId returned by begin_policy_upload. |
| forceoptional | boolean | Update 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
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "finalize_policy_upload", "arguments": { "uploadId": "<upload-id>" } }}Result
{ "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
ScopeBulk 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
| Field | Type | Description |
|---|---|---|
| policiesrequired | object[], 1–200 | Upload 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[]. |
| dryRunoptional | boolean | true → preview per-row mode + canonical validity with ZERO writes; no URLs minted. |
tools/call request
{ "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
{ "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
ScopeBulk 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
| Field | Type | Description |
|---|---|---|
| uploadIdsrequired | string[], 1–20, unique | The uploadIds returned by begin_policy_uploads (after the bytes were PUT). Max 20/call. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log rows. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "finalize_policy_uploads", "arguments": { "uploadIds": [ "<upload-id-1>", "<upload-id-2>" ] } }}Result
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| titleoptional | string, 1–200 | Ad-hoc mode: human-readable document title. Mutually exclusive with documentId. |
| documentIdoptional | string, 1–120 | Catalog mode: canonical catalog slot id (DOC-*). Creates a draft version of that slot. Publish with publish_document. Mutually exclusive with title. |
| fileNameoptional | string, 1–200 | Catalog mode only: original file name (sanitized server-side). Defaults to "{documentId}.{ext}". |
| commentsoptional | string, 0–2000 | Catalog mode only: human-visible changelog comment. |
| descriptionoptional | string, 0–2000 | Ad-hoc mode only: optional description. |
| contentTyperequired | MIME enum | MIME 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. |
| controlIdsoptional | string[], cap 20 | Ad-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. |
| frameworksoptional | string[], cap 10 | Ad-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. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| uploadIdrequired | string, 1–80 | The uploadId returned by begin_document_upload. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "finalize_document_upload", "arguments": { "uploadId": "<upload-id>" } }}Result
{ "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
ScopeBulk 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
| Field | Type | Description |
|---|---|---|
| documentsrequired | object[], 1–200 | Upload 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[]. |
| dryRunoptional | boolean | true → preview the slot-match plan (mode + catalog-slot validity) with ZERO writes; no URLs minted. |
tools/call request
{ "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
{ "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
ScopeBulk 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
| Field | Type | Description |
|---|---|---|
| uploadIdsrequired | string[], 1–20, unique | The uploadIds returned by begin_document_uploads (after the bytes were PUT). Max 20/call. |
| aiReasoningoptional | string, 1–2000 | Optional rationale recorded on the audit-log rows. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "finalize_document_uploads", "arguments": { "uploadIds": [ "<upload-id-1>", "<upload-id-2>" ] } }}Result
{ "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
ScopePublishes 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
| Field | Type | Description |
|---|---|---|
| versionIdrequired | string, 1–120 | The catalog document version id from the finalize_document_upload response. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "publish_document", "arguments": { "versionId": "<version-uuid>" } }}Result
{ "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
ScopeRecords 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
| Field | Type | Description |
|---|---|---|
| versionIdrequired | string, 1–120 | The slot's current published version id (list_documents → currentVersionId). |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "record_document_review", "arguments": { "versionId": "<version-uuid>" } }}Result
{ "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
ScopeAsks 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
| Field | Type | Description |
|---|---|---|
| messagerequired | string, 1–8000 | Your question. |
| conversationHistoryoptional | array (≤20 turns) | Prior {role:"user"|"assistant", content} turns for context. |
tools/call request
{ "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
{ "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
ScopeBulk-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
| Field | Type | Description |
|---|---|---|
| vendorsrequired | array, 1–200 rows | Vendor rows. Only `name` is required; optional fields include website, inherentRisk, category, securityOwner, businessOwner, businessPurpose, notes, dataAccess, compliance[], review dates, reviewFrequency, status. |
| dryRunoptional | boolean | true → full preview (created/updated/skipped) with ZERO writes. Run first, show the customer, then commit. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeBulk-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
| Field | Type | Description |
|---|---|---|
| risksrequired | array, 1–200 rows | Risk 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"). |
| dryRunoptional | boolean | true → full preview with ZERO writes. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeBulk-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
| Field | Type | Description |
|---|---|---|
| sourcerequired | 'vanta' | 'drata' | 'secureframe' | 'other' | The platform the export came from. |
| runsrequired | array, 1–100 rows | Run 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[]. |
| dryRunoptional | boolean | true → full preview with ZERO writes. |
| aiReasoningoptional | string, 1–2000 | Optional rationale on the audit-log row. |
tools/call request
{ "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
{ "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
ScopeList 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
| Field | Type | Description |
|---|---|---|
| statusoptional | 'active' | 'archived' | 'pending' | 'inactive' | Filter by vendor lifecycle status. Omit to list all. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_vendors", "arguments": {} }}Result
{ "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
ScopeList 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
| Field | Type | Description |
|---|---|---|
| statusoptional | 'identified' | 'mitigating' | 'accepted' | 'closed' | Filter by risk status. Omit to list all. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_risks", "arguments": { "status": "mitigating" } }}Result
{ "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
ScopeRun 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
| Field | Type | Description |
|---|---|---|
| vendorIdrequired | string | The managed vendor to assess (resolve with list_vendors). |
| dryRunoptional | boolean | true → compute and return the band with ZERO writes. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "run_vendor_risk_assessment", "arguments": { "vendorId": "vendor-imp-<sha16>" } }}Result
{ "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
ScopeRun 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
| Field | Type | Description |
|---|---|---|
| vendorIdsrequired | array, 1–25 | Managed vendor ids to assess (resolve with list_vendors). Duplicates are assessed once. |
| dryRunoptional | boolean | true → compute every band with ZERO writes. |
tools/call request
{ "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
{ "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
ScopeRecord 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
| Field | Type | Description |
|---|---|---|
| vendorIdrequired | string | The managed vendor being reviewed (resolve with list_vendors). |
| outcomerequired | 'passed' | 'passed_with_exceptions' | 'failed' | The review verdict. |
| summaryOfFindingsrequired | string, 10–2000 | What was checked and found (the CC9.2 evidence of record). |
| reviewedDocIdsoptional | array, ≤50 | Ids of this vendor's existing documents the reviewer attests to having reviewed. Ids not belonging to this vendor are dropped. |
| revieweroptional | string | Display-only name of the human reviewer, if different from the caller. Does not change the audit actor. |
tools/call request
{ "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
{ "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
ScopeUpload 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
| Field | Type | Description |
|---|---|---|
| vendorIdrequired | string, 1–200 | The managed vendor to attach the document to (resolve with list_vendors). |
| docTyperequired | docType enum | One 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. |
| namerequired | string, 1–200 | Human-readable document name, e.g. 'Example CRM SOC 2 Type II 2026'. |
| contentTyperequired | MIME enum | MIME type of the file (pdf, docx, doc, xlsx, xls, pptx, png, jpeg, gif, webp, plain, csv, json, zip). Other types are rejected. |
| contentBase64required | string (base64) | Base64-encoded file content. Max 4 MB after decode (~5.3 MB base64). |
| expiresAtoptional | string, 1–40 | Optional expiry date (ISO 8601), e.g. the SOC 2 report period end or the attestation expiry. |
| notesoptional | string, 0–2000 | Optional free-text notes. |
tools/call request
{ "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
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| vendorIdrequired | string, 1–200 | The managed vendor to attach the document to (resolve with list_vendors). Validated at begin and re-validated at finalize. |
| docTyperequired | docType enum | One 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. |
| namerequired | string, 1–200 | Human-readable document name, e.g. 'Example CRM SOC 2 Type II 2026'. |
| contentTyperequired | MIME enum | MIME 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). |
| expiresAtoptional | string, 1–40 | Optional expiry date (ISO 8601), e.g. the SOC 2 report period end or the attestation expiry. |
| notesoptional | string, 0–2000 | Optional free-text notes. |
tools/call request
{ "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
{ "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
ScopeStep 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
| Field | Type | Description |
|---|---|---|
| uploadIdrequired | string, 1–80 | The uploadId returned by begin_vendor_document_upload. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "finalize_vendor_document_upload", "arguments": { "uploadId": "<upload-id>" } }}Result
{ "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
ScopeThe 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
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_integrations", "arguments": {} }}Result
{ "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
ScopeReturns 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
| Field | Type | Description |
|---|---|---|
| integrationTyperequired | string | Which integration to connect.gwsawsslack |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "connect_integration", "arguments": { "integrationType": "gws" } }}Result
{ "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
ScopePolls a pending connection. Returns connected (with integrationId), pending, timeout, or not_found.
Ask your AI client: “Did the Google Workspace connection finish?”
Input
| Field | Type | Description |
|---|---|---|
| connectionIdrequired | string, 1–120 | The connectionId from connect_integration. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_connection_status", "arguments": { "connectionId": "<conn-uuid>" } }}Result
{ "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
ScopeList 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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id from list_audits. |
| statusoptional | string | Request status filter.requestedsubmittedacceptedrejectednarequest-moreallDefault: all |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_audit_requests", "arguments": { "auditId": "<audit-uuid>", "status": "requested" } }}Result
{ "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
ScopeOne 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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id. |
| requestIdrequired | string, 1–200 | Request id from list_audit_requests. |
tools/call request
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_audit_request", "arguments": { "auditId": "<audit-uuid>", "requestId": "<request-uuid>" } }}Result
{ "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
ScopeShort-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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id. |
| requestIdrequired | string, 1–200 | Request id. |
| attachmentIdrequired | string, 1–120 | Attachment id from get_audit_request.auditorAttachments. |
tools/call request
{ "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
{ "downloadUrl": "https://s3.amazonaws.com/...signed...", "fileName": "sample-access-review.xlsx", "contentType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "expiresInSeconds": 3600}begin_audit_response_upload
ScopeBegin 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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id. |
| requestIdrequired | string, 1–200 | Request id. |
| fileNamerequired | string, 1–255 | Original filename. |
| fileSizerequired | number (≤ 50MB) | File size in bytes. |
| contentTyperequired | string | MIME type (PDF/Office/CSV/PNG/JPEG/ZIP/text). |
tools/call request
{ "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
{ "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
ScopePrepare 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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id. |
| requestIdrequired | string, 1–200 | Request id. |
| messageoptional | string, 1–4000 | Optional note to the auditor. |
| evidenceRefIdsoptional | string[] (≤ 50) | refIds from get_audit_request.attachableEvidenceRefs. |
| uploadIdoptional | string, 1–120 | An uploadId from begin_audit_response_upload (after the PUT). |
| idempotencyKeyoptional | string, 1–120 | Usually omit. Idempotency is content-derived. |
tools/call request
{ "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
{ "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
ScopeSEND 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
| Field | Type | Description |
|---|---|---|
| auditIdrequired | string, 1–120 | Audit id. |
| requestIdrequired | string, 1–200 | Request id. |
| responseIdrequired | string, 1–120 | The draft responseId from prepare_audit_request_response. |
tools/call request
{ "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
{ "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.
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{ "type": "text", "text": "test result 'abc-123' not found" }], "_meta": { "strac.io/errorCode": "not_found" }, "isError": true }}| errorCode | Recover by |
|---|---|
| not_found | The id does not exist, or belongs to another company. Call the matching list_* tool and try a real id. |
| invalid_argument | The arguments match the schema but the values do not make sense together. Check the tool's input schema. |
| precondition_failed | The record exists but is in a state that blocks this action. Change its state first, then retry. |
| unauthorized | Your token has the scope, but your role in the company does not allow this action. Use a token from an owner or admin. |
| unknown | The tool refused the call without a category. Read the message for the reason. |
| internal | Something 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
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
- Authentication: the OAuth 2.1 + PKCE flow and discovery handshake
- REST API: script the same data with a PAT
- Strac Comply MCP: what your agent can do for you, with setup for each agent