Skip to content

MCP Server

The Logrite MCP server puts the platform inside an AI client — Claude Code, claude.ai, Claude Desktop, Cursor, ChatGPT. A connected client can read the workspace’s projects, create and watch collector agents, analyse a log file, check a repository against the workspace’s compliance frameworks, search indexed logs, and ask the Cortex knowledge packs — without anybody opening a browser, and without anybody pasting a key: the connect flow is OAuth 2.1, and the only browser step is the consent screen itself.

The tool list below matches the server’s own tool objects (scripts/dump-tools.py → docs/api-catalog/openapi/mcp.json → the Sandbox’s MCP tab), so a tool cannot be documented here and absent from the server.

SettingValue
TechnologyPython 3.12, Starlette + uvicorn, mcp SDK 1.x
TransportMCP Streamable HTTP (stateless), JSON-RPC 2.0
Port9500
RepoDeployd-io/logrite-mcp-server
Prodhttps://mcp.logrite.ai
Devhttps://dev-mcp.logrite.ai
MethodPathAuthDescription
POST/sseBearerEvery MCP method: initialize, tools/*, resources/*, prompts/*
DELETE/sseBearerSession cleanup. Stateless server, so a no-op
GET/sse—405. Stateless: there are no server-to-client streams
GET/.well-known/oauth-protected-resourcenoneRFC 9728. Named by every 401 from /sse
GET/.well-known/oauth-authorization-servernoneRFC 8414. PKCE S256 only, public clients only
POST/oauth/registernoneRFC 7591 dynamic client registration. No client secret is ever issued
GET/oauth/authorizenoneBrowser navigation → <app>/connect/<requestId>
POST/oauth/tokennoneAuthorization-code exchange (PKCE) and refresh
POST/oauth/revokenoneRFC 7009. 200 for an unknown or invalid token; 503 when the grant store or backend cannot be reached, so a revoke that did not happen never reads as done
GET/skills-packageBearerThe Claude Code plugin as a zip, with this workspace’s frameworks written in. ?for=installer with no credential returns the generic plugin (no workspace references) for the installer’s OAuth mode
GET/install.shnoneThe one-command Claude Code installer (below)
POST/coding/eventslgsi_live_The Claude Code plugin’s AI coding session events (below)
GET/coding/policylgsi_live_The workspace’s coding policy for the plugin’s local checks
GET/healthnone{"status":"ok","service":"logrite-mcp","version":"<sha>"} — version is the first 12 hex characters of the deployed commit, or unknown. No tenant data

There is no stdio server: every client, including one bridged through mcp-remote, connects over Streamable HTTP at /sse.

Point any OAuth-capable MCP client at https://mcp.logrite.ai/sse with no credential. In claude.ai or Claude Desktop that is Customize → Connectors → Add custom connector; in Claude Code it is claude mcp add --transport http logrite https://mcp.logrite.ai/sse with no --header.

  1. POST /sse answers 401 with WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource".
  2. The client reads that document and /.well-known/oauth-authorization-server, registers itself at POST /oauth/register, and opens the browser at GET /oauth/authorize.
  3. The server validates the request and redirects to <app>/connect/<requestId>, where the user signs in, picks the workspace the connection may reach, and ticks what it may do.
  4. The browser returns to the client’s own callback with a code, exchanged at POST /oauth/token.

Claude Code’s callback is http://localhost:<port>/callback on the machine Claude Code runs on. Over SSH that is the remote host, while the browser that approves is on the laptop — so after Allow the browser ends on a “can’t connect to localhost” page. The approval worked; only the last hop is missing, and it can be made by hand:

  • claude mcp login logrite --no-browser (Claude Code 2.1+) prints the sign-in URL and waits at Or paste the redirect URL here. Open the URL on the laptop, allow, and paste the address the browser ends on.
  • On the consent page, a loopback client (localhost, 127.0.0.1, [::1]) gets a ”… runs on another machine” checkbox, remembered per browser. Ticked, Allow shows the redirect URL with a Copy button instead of opening it, plus a curl -s '<url>' command for a sign-in started from /mcp → Authenticate (run it in a terminal on the remote host — that is where the listener is). Unticked, the page redirects as before and keeps the URL in the tab’s sessionStorage, so Back from the dead page shows the same panel.
  • The URL carries a one-time code that dies with the request, 10 minutes after the client started the flow. It is never sent to Logrite or logged; the panel says to treat it like a password.

One source of copy, com.logwrite.mcp.McpScopes, read by the consent screen, the tool refusal and Settings → Connections, so the three cannot drift.

ScopeTitleWhat it allows
workspace:readRead your workspaceSee the workspace, its projects, members and enabled add-ons.
logs:readRead your logsSearch and read every log index this workspace owns, including collector output.
logs:writeSend and analyse logsAnalyse log text you provide and send lines through a collector.
agents:manageManage collector agentsCreate, configure, pause and rotate collector agents.
compliance:readRead complianceSee frameworks, rule libraries, scan results, findings and the maturity assessment.
compliance:writeRun compliance workStart repository scans, write rules and policies, and record attestations.
cortex:useAsk CortexAsk questions about your data and your enabled knowledge packs.
settings:manageChange workspace settingsEnable add-ons, configure integrations and observability connections, and set retention.
destructiveDelete and revokeDelete projects, delete collector agents and purge their data, revoke API keys, and change member roles.

destructive is unticked by default. Among API keys only an admin key carries it (2026-10-14; before, no key ever did). The backend already enforces what a key may reach; withholding this one from full keys is the single rule the MCP server adds of its own, because a static key in a config file is the credential most likely to be replayed.

A tool the connection was not granted is refused with the scope named and the URL that grants it — a connected client’s tools/list never hides it. A tool that is simply absent reads to a model as a capability the product lacks, and it will tell the user so; a refusal is something it can relay and the user can act on in one click.

The one exception is an API key: tools/list shows exactly what the key’s access level can run, because no grant could ever widen it. Since 2026-10-14 that follows the key’s access — read, full or admin — and nothing else; the six “session-only” tools of 2026-10-09 are gone. See what an API key can run.

For a client that cannot do OAuth, two credential shapes, chosen by prefix:

  • A Logrite API key — lgp_live_… (or the pre-2026-09 lgs_live_…). Introspected against the backend at POST /api/v1/api-keys/introspect and cached for 60 seconds by sha256(key). This is the one a coding session holds: a session JWT lasts hours and cannot be pasted into a config file that outlives the afternoon.
  • A session JWT minted by the backend, decoded locally. Unchanged from before.

For Claude Code, use the installer: it reads $LOGRITE_API_KEY (or prompts), stores the key in ~/.logrite/api-key (readable only by you) and registers the server at user scope with a headers helper, ~/.logrite/mcp-headers.sh, so the key never appears in argv or a process list:

Terminal window
curl -fsSL https://mcp.logrite.ai/install.sh | sh

Do not pass the key with claude mcp add … --header "Authorization: Bearer lgp_live_…": the key then sits in the process list and in shell history. If you must register by hand, use claude mcp add -s user … so the server is not written into a project’s settings.

A JWT bearer must carry an exp claim; one without it is a 401.

A refused key is 401. An introspection hop that could not be reached is 503. Telling a developer their key is invalid while the backend is down sends them to create another key that will not help either, and they conclude the product is broken rather than that a hop is.

The 401 points at this estate’s app. Its “Create an API key at …/sandbox” URL comes from the estate the request arrived on (Host / X-Forwarded-Host, the same PUBLIC_ESTATES table the installer uses): dev-mcp.logrite.ai → https://dev-app.logrite.ai/sandbox, mcp.logrite.ai → https://app.logrite.ai/sandbox. LOGRITE_APP_URL is only the fallback for an unknown host.

Every OpenSearch read is scoped to the caller’s verified workspace. There is no shared default tenant: analytics_query refuses an index outside the caller’s workspace before any request leaves the server, and compliance_logs has no index argument at all — it reads only the caller’s compliance-logs-<workspaceId> through the backend’s analytics proxy, as the caller. tests/test_tenant_isolation.py pins both.

AI coding session-ingest routes (2026-10-07)

Section titled “AI coding session-ingest routes (2026-10-07)”

Two plain HTTP routes (coding_ingest.py) for the Claude Code plugin’s hooks — see AI Coding for what the plugin records and why.

  • Auth: Authorization: Bearer lgsi_live_… only — the write-only, workspace-bound session token minted by coding_sessions enrol. A missing or unknown token, a revoked one, and any lgp_live_ key, session JWT or OAuth token are 401 {"error":"unauthorized"}. The lgsi_live_ prefix is deliberately absent from API_KEY_PREFIXES, so the same token is a 401 on /sse: the two planes never share a credential. A backend that cannot be asked is 503 backend_unavailable, never a 401.
  • Token resolve goes to the backend’s GET /internal/coding/tokens/resolve?tokenHash= (sha256 of the token) and is cached in-process 60 s for a valid token, 10 s for a refusal — so a revoked token stops within a minute.
  • No CORS. The callers are hook scripts on a developer machine, never a browser page.

Body: NDJSON, gzip (Content-Encoding: gzip, or a body starting with the gzip magic bytes without the header).

ConditionAnswer
Body over 1 MB compressed, over 5 MB inflated, or more than 1000 events413 payload_too_large
Not valid gzip400 bad_request
coding.enabled off for the workspace202 {"accepted":0,"disabled":true} — nothing stored
Otherwise202 {"accepted":n,"dropped":n,"findings":n}

accepted counts documents indexed into coding-events-<workspaceId> (the workspace comes from the token, never the body); dropped is every line that was not; findings counts finding rows forwarded to the backend’s issue store (POST /internal/coding/findings, 200 per call). A failed forward is logged, not retried by the client — the events are already stored.

What is stored is decided here, not by the client: an unknown eventType is dropped; fields outside the allow-list are dropped; strings are cut to 256 characters, finding.message and finding.path to 300 and finding.snippet to 200; tools keeps at most 64 names matching ^[A-Za-z0-9_.:-]{1,64}$; the fingerprint is recomputed as sha256(ruleId|message)[:32]. The stored captureLevel is the lower of the workspace’s coding.capture and the client’s, and at metadata every finding loses message, snippet and path, keeping pathHash = sha256(path) — whatever the plugin sent.

Returns the workspace’s coding policy (settings, redaction list, rules, natural-language policies, muted fingerprints) from GET /internal/coding/policy, cached 60 s per workspace. ETag is the policy’s version; If-None-Match with it answers 304. The response carries Cache-Control: private, max-age=60.

Two groups in rate_limit_groups.py, keyed per token (tokenId) once the token is resolved:

GroupRouteDefaultOverride
coding-ingestPOST /coding/events120 / 60 sMCP_RATE_LIMIT_CODING_INGEST
coding-policyGET /coding/policy60 / 60 sMCP_RATE_LIMIT_CODING_POLICY

Thirty-three for a connected client or an admin API key (a full or read-only key sees the subset its access allows — see what an API key can run). Twelve are one tool per operation and call the platform over HTTP with the caller’s own credential, so each reaches exactly what that person reaches. The other twenty-one are grouped by object, taking an action argument that chooses the verb — collector_agent {"action": "create", …}. One tool per route would be about 120 names, and a client that lists 120 names spends its context on the list rather than on the work. Every grouped tool’s inputSchema lists its verbs in an enum, and an unknown one is answered with the valid set rather than a guess.

FamilyTools
Workspaceget_workspace_context, list_projects, get_project, enrol_repository, project_admin, project_members, workspace_members, workspace_addons, workspace_settings, api_keys
Cortexcortex_chat, knowledge_search
Logsanalyse_log_file, ingest_log_lines, semantic_search, analytics_query
Collectorscollector_agent, collector_telemetry
Compliancelist_compliance_frameworks, run_compliance_scan, scan_results, compliance_library, compliance_logs, maturity
AI Wardenwarden_providers, warden_relay, policies (Rules & Policies; replaced warden_rules on 2026-10-15)
Integrationsobservability, integrations
Auditget_mcp_activity, governance_audit
Connectionlogrite_connection
AI Codingcoding_sessions

compliance_logs reads the workspace’s compliance evidence: search (query, framework, hours, limit), services (which services write them, with counts) and summary (hours; totals by framework, control, level and service). Since 2026-10-14 it takes source — all (default), logs or warden — and all adds AI Warden’s compliance events (see compliance evidence). An index that does not exist yet answers “no logs”, not an error.

Argument names are camelCase — projectId, agentId, keyId, remoteUrl, conversationId, includeInstallCommand, includePlaintext, includePlaintextKey, templateId. The old snake_case names (project_id, id, include_token, …) are rejected, not silently ignored. Every limit is capped at 100; time windows are hours.

Every destructive action additionally takes a confirm argument that must equal the name of what it will destroy — the key’s name, the member’s email, the project’s name, the agent’s name for a collector rotate, the channel hint for integrations discord_delete. The id is never accepted as confirmation. The scope gate is the first guard and this is the second, independent one: a loop that already knows which id it is deleting cannot produce the confirmation by accident. Each tool’s description names the field confirm must match.

workspace_members invite takes the workspace name for the invitation email from the credential (key introspection returns workspaceName); the caller cannot supply it.

logrite_connection is the tool a model calls to find out what it may do: {"action": "whoami"} reports the workspace, the credential kind and the granted scopes; {"action": "scopes"} lists all nine with granted: true|false; {"action": "connect_url"} says where to go to grant more. None of the three makes a network call.

Until 2026-10-14 seven tools refused every API key locally with connected_client_required. That refusal no longer exists: the backend routes behind them accept keys (see AI Warden), and an action is gated by its scope alone. What a key lacks is refused with scope_not_granted and a text naming the key’s access level — see what an API key can run.

CaseAnswer
A tool name the server does not haveUnknown tool '<name>'. Call tools/list for the available tools., status 404
A 5th concurrent cortex_chat, analyse_log_file or run_compliance_scan for the same identityisError: true, too_many_concurrent_calls (status 429) at once — calls are not queued. At most 4 run at a time
A result whose structuredContent exceeds 32 KBTrimmed, with truncated: true, rowsBeforeCut and rowsKept; an upstream total is kept as the upstream sent it
A bad argument (wrong type, unknown action, limit over 100, an enum miss)Input validation error: <message>, isError: true, {"error":"invalid_arguments","detail":…}, recorded in the activity trail as status 400 — validated inside call_tool since 2026-10-14

The two coding_sessions / workspace_settings tools:

ToolActionScopeCalls
workspace_settingsgetworkspace:readGET /api/v1/workspaces/{ws}/settings — every setting plus the key catalogue (type, default, bounds)
setsettings:managePATCH the same route with a flat map, e.g. {"coding.capture": "metadata"}; Admin or Owner. An unknown key or bad value refuses the whole change
coding_sessionsenrolagents:managePOST /api/v1/coding/enrol — mints an lgsi_live_ session token that is never returned to the model. The result carries a one-time claimCode (claimExpiresIn: 300 s) and this estate’s mcpUrl, and the prose gives the command that stores the token locally: lr_hook.py token claim --workspace <ws> --mcp-url <mcpUrl> --code <code>
summary, issues, issuecompliance:readGET /api/v1/coding/summary, /issues, /issues/{id}
set_statuscompliance:writePATCH /api/v1/coding/issues/{id} with status (OPEN, MUTED, FIXED) and optional mutedUntil

Both take the workspace from the credential; neither has an argument for it.

Arguments as a client sends them in tools/call, and the start of what comes back.

Terminal window
# Any tool, by hand — the server speaks JSON-RPC over Streamable HTTP
curl -s -X POST https://mcp.logrite.ai/sse \
-H "Authorization: Bearer $LOGRITE_API_KEY" \
-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_projects","arguments":{}}}'
ToolArgumentsResult (text part)
get_workspace_context{}Workspace 3103 (logrite-ops). Projects: 48. Cortex add-ons enabled: Worksoft Cortex. Compliance frameworks: 30 available; enabled for AI coding here: … — counts and names only
list_projects{}Projects: 48. — each row in structuredContent is projectId, name, fullName, repoUrl, branchCount, templates, createdAt
analyse_log_file{"content": "<the log text>", "filename": "app.log"}2 finding(s). - [HIGH] Payment Service Database Outage … — the full Log Analysis API response is in structuredContent
knowledge_search{"question": "How do I create an API key?"}## Logrite product knowledge … Sandbox → API keys creates one (it starts with lgp_live_) …
cortex_chat{"message": "Summarise the errors in log-analytics-3103-app-4f9e4a"}Cortex’s answer; pass the returned conversation id as conversationId to continue the thread
semantic_search{"query": "error", "limit": 3}3 hit(s) for 'error' in log-analytics-3103-* (of 10000 matching). — with no index, the default is the workspace’s whole log family. Rows are in structuredContent
compliance_logs{"action": "summary", "hours": 24}Totals by framework, control, level and service for the last 24 hours
get_mcp_activity{"limit": 3}The last calls this workspace made through MCP (tool, outcome, duration — never the arguments)
run_compliance_scan{"repo": "Deployd-io/BankJava_TestRepo", "ref": "main", "frameworks": ["soc2"]}RUNNING with a pollUrl of /api/v1/scan-runs/latest?projectId=…; for a repo with no project, isError: true and No project in this workspace is enrolled for … Enrol it with enrol_repository first, or pass projectId.
get_project{"projectId": 4252}Project 4252: BankJava_TestRepo; repository https://github.com/Deployd-io/BankJava_TestRepo (github); 182 branch(es) known; latest scan 1316 of main on 2026-10-01T09:26:24…: 94 critical, 133 high, 47 medium, 235 low. (or never scanned); the full project is in structuredContent
enrol_repository{"remoteUrl": "[email protected]:acme/payments.git"}Enrolled acme/payments as new project …, or Already enrolled: … Nothing was created.; with no GitHub App on the owner, isError: true and Could not enrol …: The Logrite GitHub App is not installed on 'acme' … with the install URL

list_compliance_frameworks and the frameworks line of get_workspace_context read GET /api/v1/warden/library/frameworks on the backend, which accepts an API key and scopes the catalogue to the key’s workspace (30 frameworks for workspace 3103 on dev).

  • run_compliance_scan finds the project enrolled for repo (or takes projectId), maps frameworks to analyzer templates (soc2, hipaa, … — general when none; see Templates from MCP) and starts a whole-repository scan with POST /api/v1/scan/initiate. It returns the backend’s RUNNING answer and its pollUrl; poll scan_results with latest_run for the result. It writes nothing to the repository.
  • get_project reads GET /api/v1/project/{id} and enrol_repository posts to POST /api/v1/project/enrol (see API Sandbox → Project routes for machine callers). Enrolment needs the Logrite GitHub App on the repository’s owner: connect it at Settings → Workspace → Source control.
  • Every failure sets isError: true — an upstream error never comes back looking like an answer.

Full argument schemas are in the product at Sandbox → MCP → Tools, which renders them from the same generated index rather than from a second copy.

Every tool returns one readable TextContent block and the MCP result’s structuredContent field, so an agent that needs a count does not have to re-parse a formatted table. The data is not repeated as a second JSON text block. A list payload is wrapped as {"items": […]}, because the spec requires an object. structuredContent is trimmed to 32 KB, with truncated: true and total: N when it was cut; page with limit (and after for analytics_query). An error carries structuredContent too (e.g. {"error": "not_found", …}) alongside isError: true.

No internal addresses in a response. A section’s source names the public route a developer can call with the same key (the Logrite backend /api/v1/…), and outage and error messages are scrubbed of every internal base URL.

A template is a project’s scan profile (a scan_profile policy, see Rules & Policies) for one compliance framework: one per framework key (soc2, hipaa, pci, gdpr, m2614, general, …), copied from the global defaults when it is added, and afterwards the project’s own (edit its rules in the app, Generator → Configure Rules, or with policies update). Template ids are scan-profile policy ids. A template can also hold only the rules you choose — “SOC 2, but only method entry, method exit and database calls” — over MCP as well as in the app (2026-10-13). A scan runs the templates its frameworks name.

Four project_admin actions manage them; each takes projectId:

ActionScopeBackend routeWhat it does
list_templatesworkspace:readGET /api/v1/project/{projectId}/templatesThe project’s templates (name, key, log type, rule count, templateId) and addable: the framework keys with global rules the project does not have yet
list_rulesworkspace:readGET /api/v1/project/{projectId}/templates/rules?framework=<key>frameworks: one name. The rules a chosen-rule template from that framework may hold: the framework’s own, then general’s (entry, exit, exceptions, variables, database and external calls)
add_templatescompliance:writePOST /api/v1/project/{projectId}/templatesframeworks: 1–20 names or keys, each ≤64 characters. Adds the known ones, reports the rest; never fails part-way. With name + rules (one framework): a template holding only those rules — see below
remove_templatedestructiveDELETE /api/v1/project/{projectId}/templates/{templateId}templateId plus confirm = the template’s name. Irreversible
project_admin {"action":"list_templates","projectId":5952}
→ Project 5952 templates: SOC 2 (key 'soc2', <n> rules, id <templateId>), General (key 'general', …).
Can be added: gdpr, hipaa, … — pass them to add_templates as frameworks.
project_admin {"action":"add_templates","projectId":5952,"frameworks":["gdpr","nope"]}
→ Added GDPR (<n> rules) to project 5952. Not a framework: nope. Addable: hipaa, ….
  • add_templates answers added, existing (already on the project), unknown (names that resolve to no framework with global rules) and the remaining addable. When nothing was added and nothing already existed, the result is isError with error: template_unknown.
  • remove_template needs ‘Delete and revoke’. It needs the destructive scope, which only an OAuth connection granted it or (since 2026-10-14) an admin API key holds; a full or read key’s tools/list does not offer it and a direct call is refused (scope_not_granted, naming the admin API key). An unknown templateId answers template_not_found. Removing the last template is allowed; the next scan seeds general.
  • Who may change templates. Adding and removing need the workspace’s policy-code-write role floor (Developer by default; the separate template-write floor was removed on 2026-10-21, backend 7db0d5a1); a Viewer gets 403. Both are written to the governance audit log (project_templates_added, project_template_removed). A template is a scan profile: templateId is the scan-profile policy id, also accepted by policies get / delete (MCP 37f82f7d).
project_admin {"action":"list_rules","projectId":6355,"frameworks":["soc2"]}
→ 21 rule(s) a 'soc2' template can hold: soc2_logical_access (…, soc2_compliance); …;
method_entry_logging (Log Method Entry, general); method_exit_logging (…); database_logging (…)
project_admin {"action":"add_templates","projectId":6355,"frameworks":["soc2"],
"name":"SOC2 entry-exit-db",
"rules":["method_entry_logging","method_exit_logging","database_logging"]}
→ Created template 'SOC2 entry-exit-db' (id 840, soc2_compliance) on project 6355 with only 3
rule(s): method_entry_logging, method_exit_logging, database_logging.
Scan with it by passing frameworks: ['SOC2 entry-exit-db'].
  • A rule is named by its canonical name or its display name, in any case or spacing.
  • All or nothing. An unknown rule refuses the whole template and lists the choosable ones. A template missing a rule you believe it holds would scan clean for that rule.
  • Half a request is refused, never widened. rules without name, name without rules, or two frameworks is a 400 — it never falls through to adding the full framework.
  • The name is an identifier (a scan selects the template by it), so it may not match a template already on the project or any framework key: frameworks: ["soc2"] keeps meaning the framework.
  • Any argument add_templates does not take (e.g. ruleIds) is refused with unknown_arguments and nothing is sent. Until 2026-10-13 it was silently ignored and the full framework was added.
  • Audited as project_template_created, with the chosen rules. Same policy-code-write floor.

A name is matched case-insensitively with spaces, _ and - removed and a trailing compliance or audit dropped, so soc2, SOC 2, soc2_compliance and SOC2 Type 2 are one template, and pci, PCI DSS, PCIDSS and pci_compliance are another. Two spellings of the same framework in one request add it once.

Valid keys come from project_admin list_templates (addable) or from list_compliance_frameworks (libraryKey). ops is listed by list_compliance_frameworks but is operational log triage, not a code framework: it has no code rules and cannot be scanned or added.

run_compliance_scan with a framework the project does not have adds that template from the global defaults and says so — the backend’s seededTemplates names what it created, and the reply reads Added template(s) HIPAA to project 5952 from the global defaults; they stay on the project.

If any requested name is not a framework at all, the scan is refused with template_not_configured before anything is saved: nothing is added and nothing is scanned. The refusal lists the templates the project has and the framework keys that can be added, and tells you to pass one in frameworks or add it first with project_admin add_templates.

Keeping some logs and opening a pull request (2026-10-13)

Section titled “Keeping some logs and opening a pull request (2026-10-13)”

After a scan, two scan_results actions finish the flow the app’s Generator runs: keep some of the recommended log statements, then write them to a branch and open a pull request. Both need compliance:write and work with an API key or a connected client; the backend applies its own role floor (Developer for generation).

ActionBackend routeWhat it does
approveGET then POST /api/v1/scan-results/{scanId}Keeps statements matching severities, rules (canonical or display name) and files (path contains); drops any matching exclude (a rule or a path). Without confirm it is a preview and sends nothing. With confirm = the project’s name it approves
generatePOST /api/v1/generatorscanId + confirm = the project’s name (read back from the server). Writes a branch and opens a PR from the approved statements — or from all of them if the scan was never approved, and it says which
scan_results {"action":"approve","scanId":1405,"rules":["method_entry_logging"]}
→ Preview, nothing approved: scan 1405 (carevault-care-management-agent) would keep 68 of 134
recommended log statements (critical 0, high 0, medium 0, low 68). Approving is final for this
scan — the other 66 are removed from it. To approve, call again with confirm '<project name>'.
scan_results {"action":"generate","scanId":1405,"confirm":"carevault-care-management-agent"}
→ Generation started for scan 1405 (…) from the statements approved on it. …
scan_results {"action":"latest_run","projectId":6355}
→ Run 1738 (GENERATE) for project 6355: COMPLETED, stage 'pr_push'; results in scan 1405. …
Pull request: https://github.com/Deployd-io/carevault-care-management-agent/pull/7
(branch feat/add_logging_…); 67 of 68 log statements placed.
  • Approval is final for that scan. The backend overwrites the scan’s detail maps with what was kept, so a dropped statement is gone from that scan; a new scan brings it back. That is why approve previews by default. A scan already generated from is refused (already_generated).
  • Approval is built from the RAW scan, never from get_result’s output, which strips absFileName to a basename (MCP-5) — the generator needs the real path.
  • The PR link comes from the generator’s record. The run ledger’s GENERATE row carries no PR (prNumber stays null), so latest_run / get_run read branchLink and runDetails.counts from GET /api/v1/log-generator-results/project/{id} for a COMPLETED GENERATE run.
  • Generation is still never a side effect of a scan (MCP-5). run_compliance_scan refuses mode, push, createPr (generation_not_supported) and names these two actions instead.

Verified end to end on dev (ws 2905, project 6355): template 840 (3 rules) → scan 1405 found only method exit (66) and method entry (68) → approve kept the 68 entry statements → PR #7 added 67 entry logs (one pruned by a syntax gate) and nothing else.

  • A call is validated against its own caller’s catalogue. The MCP SDK validates arguments against one tool cache on the shared server object, refilled by whichever caller listed tools last. Because an API key is shown a narrowed catalogue, a signed-in (OAuth or session) user was refused every session-only action — Input validation error: 'verify' is not one of ['list'] — whenever an API-key client had listed more recently. mcp_server.PerCallerServer now looks the tool up in registry.tools_for(<this caller>) on every call. Verified live on dev: a session’s governance_audit verify called straight after an API key listed works 6 times out of 6, and the key itself is still refused.
  • Three actions now say what the backend requires, and check it before sending:
    • warden_rules preview_custom needs title as well as sourceText (nothing is saved). Since 2026-10-15 this is policies preview_logging_rule.
    • compliance_library add_attestation needs status (attested or not_applicable, sent as ATTESTED / NOT_APPLICABLE), owner, a note for not_applicable, and an attestable obligationId from list_attestations.
    • warden_relay suppress_violation needs framework and rule; requestId and agentId only narrow it.
  • Sandbox → MCP lists the current actions. Its index (generated 2026-10-09) predated list_templates, list_rules, add_templates, remove_template, approve and generate; regenerated from docs/api-catalog/openapi/mcp.json.

From the all-tools use-case run (178 calls, /mcp-usecases/; the MCP now has 33 tools — warden_providers was added the same day):

  • warden_rules preview_custom (now policies preview_logging_rule) flags a failed compile as compile_failed (it used to say “The rule previewed.”), and says “compiled” when it did.
  • warden_relay get_request reads the {request: …} envelope and names verdict, layer, risk, rules, model and the reason (the judge’s reasoning when that decided it).
  • api_keys introspect on a session or OAuth connection describes that credential instead of relaying the backend’s bare 401 {"active": false}.
  • scan_results key_findings on a scan whose AI summary step failed says so and gives the recorded counts, instead of “recorded no key findings”.
  • analytics_query search prose keeps 300 characters per hit (was 160), enough to show a .NET exception’s status code.
  • governance_audit verify reports recovered legacy rows, unverifiable rows and forks by id, and never says “all N verified” over any of them.
URIContents
logrite://workspace/contextWorkspace, role, projects, add-ons, and (since 2026-10-14) AI Warden status
logrite://compliance/frameworksThe frameworks and rule libraries in force
logrite://knowledge/<pack>One per enabled Cortex knowledge pack

A pack the workspace has not enabled is not listed. An entitlement that advertises itself and then refuses is worse than one that is simply absent. Reads are re-checked against the live listing, so an add-on disabled mid-session stops being readable.

Ten, each read from a plugin/commands/*.md file: init, compliance-check, log-review, explain-error, add-logging, and since 2026-10-14 data-sources (what LogRite can see), recent-errors, what-happened, warden-setup (set up AI Warden step by step) and warden-report (a 7-day AI Warden report). The five new ones mirror the Sandbox chat presets.

📐 How the skills and the MCP fit together — a single-page architecture map: the two planes, the install and load path, the request lifecycle, the tools with their downstream routes, tenant scoping, the activity document, the session timeline and the UI reflection map. Self-contained, and the place to start if you have never seen this surface.

plugin/ in the MCP repo, beside the tool definitions it documents — so the tool list and the skills describing it cannot drift apart. It carries thirteen commands (init, fix, privacy, compliance-check, log-review, explain-error, add-logging, uninstall, and since 2026-10-14 data-sources, recent-errors, what-happened, warden-setup, warden-report), five skills (logrite-logging, logrite-compliance, logrite-observability, logrite-warden since 2026-10-14, and worksoft-certify only when the workspace has the worksoft-cortex add-on) and hooks on five events (plugin/hooks/hooks.json).

The checked-in manifest declares the MCP server inline, so a plugin installed by hand is also connected. The installer takes the other road deliberately: it strips mcpServers and userConfig out of the package it builds and registers the server once itself, because two registrations of the same tools list every tool twice — logrite beside plugin:logrite:logrite.

Two ways to get it, both offered from Sandbox → MCP.

Terminal window
curl -fsSL https://mcp.logrite.ai/install.sh | sh

The key comes from LOGRITE_API_KEY when it is set on sh, otherwise from a hidden prompt on the terminal:

Terminal window
curl -fsSL https://mcp.logrite.ai/install.sh | LOGRITE_API_KEY=lgp_live_… sh

The key is never a command-line argument; the installer refuses one.

  • Key mode (a key was given). The key is validated against the backend before anything is written, then stored in ~/.logrite/api-key (mode 0600). Claude Code reads it through a headersHelper script, ~/.logrite/mcp-headers.sh, so the key is never in a process argument or in Claude Code’s MCP config.
  • OAuth mode (an empty answer at the prompt, no terminal and no LOGRITE_API_KEY, or LOGRITE_AUTH=oauth). The server is registered with no credential — claude mcp add -s user --transport http logrite https://mcp.logrite.ai/sse — the plugin is installed, and any earlier key file and helper are removed. Start Claude Code, run /mcp, choose logrite, then Authenticate, and sign in in the browser. When SSH_CONNECTION or SSH_TTY is set, the installer says to use claude mcp login logrite --no-browser instead (see Signing in over SSH). LOGRITE_AUTH=oauth is how a key install switches to sign-in: while the key is configured, Claude Code sends it over the token.

In both modes the installer unpacks the plugin into a local marketplace at ~/.logrite/claude-marketplace, registers the MCP server at Claude Code’s user scope (every repository), and runs claude plugin marketplace add and claude plugin install logrite@logrite-local -s user. Then, in a repository, type /logrite:init.

It refuses to run as root, never echoes the key, and is re-runnable. A key check that fails says which kind of failure it was:

What happenedWhat it says
401the key was not accepted — create a new one
no answer at alla network problem, not a problem with your key

If the plugin install step fails, the installer says the skills, the /logrite: commands and the hooks are not active and prints the two commands to finish it.

The script is served only by the MCP host (https://mcp.logrite.ai/install.sh, or https://dev-mcp.logrite.ai/install.sh for the dev estate). Each host fills in its own estate’s URLs, so no variables are needed; to point it elsewhere by hand:

VariableDefaultUsed for
LOGRITE_MCP_URLhttps://mcp.logrite.aiThe MCP endpoint registered with Claude Code, and the plugin download
LOGRITE_BACKEND_URLhttps://leapfrogapi.deployd.ioValidating the key (/api/v1/api-keys/introspect) before anything is written
LOGRITE_APP_URLhttps://app.logrite.aiWhere error messages send you to create a key
LOGRITE_API_KEY—The key. Unset: the installer prompts, and an empty answer selects OAuth mode

To undo: /logrite:uninstall in each repository, then claude plugin uninstall logrite@logrite-local, claude plugin marketplace remove logrite-local, claude mcp remove -s user logrite, and remove ~/.logrite/claude-marketplace, ~/.logrite/api-key and ~/.logrite/mcp-headers.sh.

Sandbox → MCP downloads logrite-claude-plugin.zip, pre-filled with this workspace’s frameworks. Install it with:

Terminal window
mkdir -p ~/.logrite && unzip logrite-claude-plugin.zip -d ~/.logrite/claude-marketplace
claude plugin marketplace add ~/.logrite/claude-marketplace
claude plugin install logrite@logrite-local -s user

The zip’s root is the logrite-local marketplace, with the plugin under logrite/. A plugin installed this way declares the MCP server itself without a key (plugin 1.3.5): it connects over OAuth — run /mcp and choose Authenticate. To use an API key, install with the installer instead.

Every hook is one dispatcher, lr.sh <event> → lr_hook.py (python3 3.8+, stdlib only): session_start (injects the workspace, frameworks and add-ons) and an async flush on SessionStart; nudge (points an error-shaped prompt at knowledge_search, only when the repository has add-ons) and prompt on UserPromptSubmit; edit (PostToolUse, matcher Write|Edit|MultiEdit, scans the lines added to the edited file); bash_edit (PostToolUse, matcher Bash, scans files a shell command changed); stop (records the turn, scans every file the turn changed — including through Bash — and may ask the agent for one fix round) and flush on Stop; session_end (spool only). flush ships the spool to POST /coding/events. The logging checks come from the workspace’s coding policy, with a built-in copy as the offline fallback. Details: AI Coding → The hooks.

Plugin releases since the first coding-sessions build:

VersionChange
1.1.2A healed finding is recorded as found, then fixed (fixedInSession: false, then true)
1.1.3Stop also scans files changed through Bash — git-modified or untracked files with an mtime at or after the turn’s start
1.1.4A reference to a secret (${DB_PASSWORD}, os.getenv(…), {{ … }}, ***) is not a credential finding — except inside a log call, where only masks are exempt; ${NAME:default} with a non-empty default is still a finding
1.1.5A {} placeholder after a field name is not a value: log.info("… email={}", user) is recorded as written instead of email=***} (same change in ai-service shared/redaction.ts)
1.2.0Installer key and OAuth modes, the key kept in ~/.logrite/api-key; /logrite:init is the only init command; frameworks and add-ons read from the workspace; local-only config when enrolment is refused; the shell hooks folded into lr_hook.py
1.2.1The scanner skips the plugin’s own config
1.2.2The Stop scan covers what the turn committed
1.3.0Multi-line log statements; .logrite/ skipped in added lines; heal wording and next-turn fixes; skills for the enabled frameworks
1.3.1/logrite:init prints the non-interactive line verbatim (released with the template-visibility fixes, K13)
1.3.2A fix made in a claude -p --continue turn counts as fixed in session; session_end always ships its spool
1.3.3Line stats against the turn’s base; edit-time heals recorded; no blank-session events; repository label from the git remote; /logrite:init pre-approvals; a secret-in-log finding needs a logged value; log[level](…) calls
1.3.4Sign-in over SSH: refusals name claude mcp login logrite --no-browser; LOGRITE_AUTH=oauth switches a key install off its key
1.3.5The zip plugin is OAuth-only (no headers, no api_key setting); the installer exits 130 on Ctrl-C and keeps the stored key
1.3.6Five new commands and prompts (data-sources, recent-errors, what-happened, warden-setup, warden-report); the logrite-warden skill (2026-10-14)
1.3.7The policies tool replaces warden_rules; /internal/coding/policy takes the enrolled projectId and the plugin caches policy-<projectId>.json (or policy-<ws>.json without a project); AI-coding frameworks picked by enabledFeatures containing ai_coding (mcp-server e05302a, 2026-10-15)
1.3.8Released with the Rules & Policies audit fixes: policies gains overview, effective_all, activity and changes and pages list; preview_logging_rule needs compliance:write; get_workspace_context enabledFrameworks = AI-coding frameworks, plus frameworksByFeature and a policies section; /logrite:init text matches (mcp-server dbcba7fc, 2026-10-21, dev and prod)

Every one exits 0 when the network, the token, python3 or the config is missing — lr.sh exits 0 silently with no python3, and lr_hook.py swallows every exception. A hook that blocks a developer’s edit because a SaaS was unreachable gets the plugin removed, and the developer is right to remove it.

.logrite/config.json in the repository root: workspace, project, label, frameworks, add-ons, mcpUrl, and recordSessions once recording is answered. It enrols the repository by its origin remote (or the only remote) — re-running finds the existing project rather than creating a second. It writes nothing into .claude/settings.json: the hooks ship with the plugin.

When enrolment is refused (400 or 409, typically because the Logrite GitHub App is not on the repository’s owner) or the repository has no remote, init writes a local-only config with "projectId": null. Local checks still run; server scans and pull-request checks need the GitHub App — connect it at Settings → Workspace → Source control → Link existing installation, then run /logrite:init again.

If the developer agrees to record AI coding sessions, it also writes the write-only session token to ~/.config/logrite/coding/<workspaceId>.token (mode 0600, outside the repository). A warn-only git pre-commit check (a marked block in .git/hooks/pre-commit, or the core.hooksPath hook) is offered in the same question, never written without a yes. /logrite:uninstall removes the pre-commit block and asks before deleting the token, spool and cache. See AI Coding.

Every JSON-RPC request writes one document to mcp-activity-<workspaceId>: timestamp, workspace, user, credential kind and key last-4, method, tool, argumentsDigest, outcome, duration, result size, client and source IP.

  • Arguments are hashed, never stored. A tool call carries customer log content and search terms; an audit index holding them is a second copy of the data with none of the retention rules of the first, in an index nobody thinks of as data.
  • Writes are fire-and-forget on a bounded queue, with a mcp-activity write dropped counter. An audit write that can fail a tool call turns logging into an outage.
  • mcp-activity is a tenant family in both config.py and com.logwrite.util.IndexNames. That list is duplicated across two languages and a one-sided edit is silent. compliance-logs (the per-workspace compliance-logs-<ws> family compliance_logs reads) is registered on both sides too. Absent from IndexNames, the OpenSearch proxy’s isTokenAllowed would treat it as “not a tenant index” and let any workspace read it.
  • Key lifecycle does not go here. api_key_minted / rotated / revoked are GovernanceAudit rows in the backend — hash-chained and low-volume. A coding session issues hundreds of tool calls a minute; hash-chaining that would be the wrong instrument.

Read it at GET /api/v1/mcp/activity?workspaceId=&since=&until=&tool=&limit=&from= on the backend (from 0–9500, else 400 “from must be between 0 and 9500”; added 2026-10-14), or in Sandbox → MCP → Activity. get_mcp_activity pages with it — see paging the activity trail.

VariableDefaultPurpose
MCP_PORT9500Listen port
SECURITY_JWT_SECRET—HMAC key for the JWT path; mirrors the backend’s base64 decode
LOGRITE_BACKEND_URLhttp://backend:8080Key introspection, projects, frameworks, enrolment
LOGRITE_AI_URLhttp://ai-service:9300Cortex chat, log analysis, knowledge packs
LOGRITE_SEMANTIC_URLhttp://semantic-search:9100Hybrid search
LOGRITE_INGEST_URLhttp://ingest-shim:9700Collector ingest
LOGRITE_APP_URLhttps://app.logrite.aiWhere a 401 sends the caller to create a key — only for a Host not in PUBLIC_ESTATES; a known estate gets its own app URL
LOGRITE_WARDEN_URLhttps://warden.logrite.aiThe AI Warden relay base URL named in warden_relay prose — only for a Host not in PUBLIC_ESTATES; a known estate gets its own (https://dev-warden.logrite.ai on dev)
OPENSEARCH_URLhttp://localhost:9200The cluster
MCP_API_KEY_CACHE_TTL60Seconds a verified key stays resolved in-process
MCP_ACTIVITY_ENABLEDtrueThe audit trail
MCP_ACTIVITY_QUEUE_MAX1000Bounded queue; overflow is counted and logged
MCP_OAUTH_SIGNING_SECRET—HS256 key for this server’s own tokens. Deliberately not SECURITY_JWT_SECRET
LOGRITE_INTERNAL_TOKEN—X-Warden-Internal-Token on /internal/mcp/**; must equal the backend’s warden.internal.token
MCP_OAUTH_ACCESS_TTL3600Access-token lifetime, seconds
MCP_OAUTH_REFRESH_TTL2592000Refresh-token lifetime, seconds (30 days)
MCP_GRANT_CACHE_TTL60Seconds a resolved grant stays cached; a revoke takes effect within it
Terminal window
python -m pip install -r requirements-dev.txt
python -m pytest tests -q

tests/test_auth_credentials.py pins the credential shapes and the 401/503 distinction; tests/test_tenant_isolation.py demonstrates isolation and fails if the auth bypass returns. Both run in CI on the self-hosted logrite-dev runner.

The unit suite mocks the hops. ops/testing/mcp_acceptance.py in LogriteMasterControl drives the deployed server over the tunnel instead — every protocol method, every auth outcome and every tool, against a named estate, with one stated expectation per case:

Terminal window
python3 ops/testing/mcp_acceptance.py --estate prod # ops key, workspace 252
LOGRITE_DEV_API_KEY=lgp_live_… \
python3 ops/testing/mcp_acceptance.py --estate dev # workspace 3103
python3 ops/testing/mcp_acceptance.py --estate prod --only T7,T23 --json

Exit 0 only when every applicable case passed, 1 on any failure, 2 when the estate’s credential is missing — it runs nothing in that case, because a harness that silently skips is worse than one that refuses. Stdlib only, so it does not need this service’s venv.

Three properties are load-bearing and easy to lose in a refactor:

  • Evidence is printed on PASS as well as on fail. A pass with the wrong evidence beside it is how an assertion that asserts nothing survives review.
  • The key is never printed, and every response body is checked for it. A credential echoed into a tool result ends up in a transcript, and a transcript ends up in a ticket.
  • Order is fixed — T16 before T17–T19 (the compliance read needs the write that precedes it), T7 before T23 (the trail case asserts the refusal T7 just caused). The cases are an ordered list, never a dict.

A case that only applies to one estate prints SKIP with its reason and is not counted as a pass: T16 is dev-only (ingesting on prod would grow an index for a test, and the code path is identical), T23 is prod-only (it asserts the 403 that only prod’s un-entitled workspace produces).

Measured 2026-10-06 against logrite-mcp 1.30.0: dev 31 passed / 1 skipped / 0 failed, prod 30 / 1 / 1. The single prod failure is T8 cortex_chat, and it is not an MCP defect — see below.

Session-only tools are hidden from API keys

Section titled “Session-only tools are hidden from API keys”

tools/list for an API-key caller omitted the six tools an API key could not use — warden_rules, warden_relay, compliance_library, maturity, governance_audit, observability — and offered api_keys with the single action introspect. A connected client (OAuth) still saw all 32 tools. Each of these tools’ descriptions ended “Needs a connected client (OAuth); not available to API keys.”

Claude Code drops structuredContent on error results, so every isError result’s text ends with the code and its links:

<human sentence>
error: scope_not_granted
manage_url: https://app.logrite.ai/settings?tab=connections
scope: destructive

The error: <code> line appears when structuredContent.error is an identifier, followed by one line for each present key of connectUrl, upgrade_url, manage_url, addonKeys, scope, in that order. knowledge_search’s 403 carries addonKeys (e.g. worksoft-cortex). Hints that used to live only in prose (nextHint cursors, emptyHidden on list_indexes) are also structured keys.

  • aggregate resolves the field first: a keyword/numeric/date field is used as is; a text field with a .keyword sub-field becomes <field>.keyword (translatedFrom says so); a field that does not exist is unknown_field (with up to 30 fields you can group by); text without .keyword is field_not_aggregatable. The result is {index, field, translatedFrom, total, buckets:[{key,count}], otherCount}.

  • Queries containing script, script_score, scripted_metric, runtime_mappings, script_fields or derived anywhere are refused (400) before any call. The backend proxy refuses the same keys from API keys and connected clients too (invalid_fields); the app’s own session is unaffected.

  • Log text is masked before it reaches the client (MCP 1153b93). search hit text, aggregate bucket keys and every string in semantic_search hits go through tools/pii_mask.py, a port of the backend’s CollectorLogRedactor: ssn=, password=, token=-style values, SSNs, emails, sk- keys and IBANs become ***; card-shaped numbers only when they pass the Luhn check, so epoch-millisecond timestamps stay readable. Before it, an uploaded ssn=123-45-6789 came back raw while the same upload’s findings showed [SSN-1]. Grouping by an email field therefore shows *** keys; the counts are unchanged.

semantic_search refuses a blank query and one over 1,000 characters, passes the API’s validation details through, and when the API reports mode: "keyword" says “keyword matches only: these indexes have no embeddings, so wording must overlap” — or, since MCP 0d91266, when the API reranked the word matches (ranking: "meaning"), “found by shared words, ranked by meaning”, and with recall: "words+meaning" (MCP 7cdefdc) “found by shared words and by log messages close in meaning”. observability’s secrets names each vendor’s fields (Datadog apiKey+appKey) since MCP 6dece65. Collector log indexes are keyword (BM25); uploads created since 2026-10-09 have embeddings — see Semantic search.

  • list_projects takes limit (1–100, default 100) and offset, returns {items, total, nextOffset}; rows carry branchCount but not branch names (get_project has them). nextOffset is offset plus the rows actually sent: when the 32 KB budget trims a page, the next page starts at the first row that was cut (mcp-server d9f0fa8b; before it, rows past the cut were unreachable). The same holds for scan_results workspace_summary.
  • scan_results workspace_summary projects each row to the scan’s id, project, branch, language, run date, severity totals and coverage, takes limit (1–100, default 50) and offset, and says “latest scan for M project(s); showing N (a–b)”.
  • Whenever a result is cut to fit the 32 KB budget the text says “structuredContent cut to fit: showing n of total rows; narrow the request or page.”
  • scan_results details: page ≥ 0 and limit 1–100 or the call is refused.
  • Numbers name their units. Summary totals are outstanding recommended log statements; details count finding groups (one per file × logging rule) and also report statementCount and outstandingStatementCount.
  • Summary, details and get_result carry no server paths: checkoutFolderPath is dropped and files are relative (filePath).
  • run_compliance_scan / get_run prose reads the run state: “Run 812 (scan) for project 4002: FAILED, stage ’…’. Failed: …”. Refusals now arrive before anything is marked running: branch_not_found (the named branch does not exist), scan_in_progress (a run is already active for the project), template_not_configured (a requested template name exists nowhere; nothing is scanned).
  • A requested framework the project has no rules for (e.g. ["soc2","hipaa"] on a SOC2-only project) is seeded from the global defaults on first use and scanned, and the reply says so (see Templates from MCP); appliedTemplates is always filled and the label reads SOC 2+HIPAA, never a blank “GENERAL”. A project with no rules at all is seeded the same way.
  • Owners who never signed in with GitHub can scan: the backend falls back to the GitHub App installation token of the repository.
  • get_workspace_context returns workspaceName, mcpUrl, enabledFrameworks — since MCP 792c30a0 (2026-10-21) only the frameworks whose enabledFeatures contain ai_coding, which is what /logrite:init writes; before, every enabled framework was called “enabled for AI coding” — and frameworksByFeature: {feature: [libraryKey…]}. It also carries a policies section from /api/v1/policies/overview: {state, byStatus, byKindStatus, attention: [code…], attentionCounts: {code: n}}, each attention code once. Its text started Workspace 3103 (logrite-ops).; since 2026-10-14 it reads Workspace logrite-ops (3103). and adds an AI Warden line.
  • list_compliance_frameworks rows are slim (libraryKey, displayLabel, kind, region, tier, version, ruleCount, enabled, enabledFeatures), so all fit; prose names the enabled ones. rules no longer requires libraryKey: without it the backend lists every library (MCP 37f82f7d).
  • enrol_repository returns mcpUrl.
  • knowledge_search answers “No entry in the enabled packs covers that.” with matched:false when no topic matches, instead of an unrelated topic.
  • collector_telemetry: hours is 1–8760; only logs and flags accept 0 (all time). An argument the action does not take, or from later than to, is refused. Log/flag prose states whether reveal was honoured (“reveal was requested but NOT honoured …” for API keys).
  • collector_agent collectPath: refused if it contains quotes, backticks, $, backslashes or control characters; a path that does not look like a plain absolute folder is accepted with a warning.
  • workspace_addons checks addonKey against ^[a-z0-9][a-z0-9-]{0,63}$.
  • integrations salesforce_cases says when no Salesforce org is connected.
  • api_keys introspect and other prose never print “None”.
  • workspace_settings lists and accepts the privacy keys (warden.redaction.*, cortex.redaction*, warden.sensitiveRawCapture) next to ingest.redaction.* — see Settings.
  • Revoking an OAuth connection stops its delegated tokens at the backend within about 5 s (grant status cache), not after the MCP grant cache’s 60 s.
  • Templates are visible (K13). list_projects rows and get_project carry templates as objects with name, logType and key. Template names are accepted in several spellings: for example PCI DSS, pci_compliance, pci and PCIDSS all resolve to the same template.
  • A single framework is seeded (AJ-15). run_compliance_scan with one framework sends it as template, and the backend seeds that template on first use exactly as it does for a list.
  • Hints name tools API keys can call (AJ-17). Refusal hints no longer point at cortex_chat (closed to API keys); they point at the Logrite app (Generator → Configure Rules).
  • knowledge_search says what it searched (K14). A no-match reply reads “No entry in the enabled packs covers that.” and its structuredContent carries packsSearched (the packs actually searched, never empty when packs were consulted) and disabledAddons (add-ons that would cover the question but are not enabled). The prose names a disabled add-on when one matches.
  • api_keys for API keys. With an API key the tool’s description says only introspect is available and why.
  • Revoked grants stop in seconds. The OAuth grant cache TTL is 3 s, and a backend 401 grant_revoked evicts the grant at once; the tool result is isError ending with error: grant_revoked.
  • Scan details state their scope. scan_results details reports detailsScope, saying whether the outstanding counts come from the scan’s own findings or from the generator’s approval ledger.

AI Warden end to end, and the 13 October test report (2026-10-14)

Section titled “AI Warden end to end, and the 13 October test report (2026-10-14)”

The MCP test report of 13 October 2026 found that a customer could not run AI Warden through MCP: an API key was offered Warden actions that failed live, compliance events were invisible, a time window on them returned 0, and a missing index answered 0 hits. Everything below shipped to fix that and the report’s other findings. MCP commits ff61ea9, b0eae9c, 4054f16; backend 2f3e3cea, 2dc58675, cd061778; ai-service cc9175e; frontend 02ed9614.

An lgp_live_ key carries an access level, read from POST /api/v1/api-keys/introspect (auth.verify_api_key puts it on the identity; a key without one is full). The level decides the scopes the key holds (tools/scopes.granted):

AccessScopesMinted by
readworkspace:read, logs:read, compliance:read, cortex:use (READ_SCOPES)an Owner/Admin in the app, or a credential (below)
full (default)every scope except destructivethe same
adminall nine, including destructiveonly in the app, by a signed-in Owner or Admin

OAuth connections are unchanged: they hold what their grant holds. A session JWT and a coding token hold every scope except destructive.

tools/list for a key shows exactly what it can run (tools/registry.tools_for). A platform tool whose scope the key lacks is dropped; a grouped tool keeps only the actions it can run, with the action enum, the action description’s ; clauses and the action-only properties narrowed to them; a tool with no runnable action is dropped. A narrowed description is the tool’s first sentence plus one note:

  • every hidden action is destructive: “rotate_key/revoke_key need an admin API key or a client connected with ‘Delete and revoke’.”
  • otherwise: “This API key is read-only; create/… need a full key.”

An admin key’s list equals the full catalogue. Every grouped tool’s action description is now ;-separated clauses, each headed by the actions it describes (name[/name…]: …), and a catalogue test proves each head names only that tool’s actions.

A direct call to an action the key lacks is refused by scopes.require with scope_not_granted, status 403, and a text about the key, not about a grant:

KeyText
full, destructive neededThat needs 'Delete and revoke', which a full API key never holds. Use an admin API key (an Owner or Admin creates one at https://app.logrite.ai/sandbox → API keys → Access: Admin), or connect this client with OAuth and grant 'Delete and revoke': curl -fsSL https://mcp.logrite.ai/install.sh | LOGRITE_AUTH=oauth sh.
readThis API key is read-only, so it cannot run policies create ('Run compliance work'). Use a full API key, or connect with OAuth.

structuredContent carries error, scope, scope_title, granted, access, switchToOAuth, sshLogin and manage_url. SESSION_ONLY, session_only(), _refuse_api_key, SESSION_NOTE, _mark_session_only and the connected_client_required refusal are deleted: the backend routes they guarded accept keys now (see AI Warden).

logrite_connection list_connections (workspace:read) and revoke_connection (destructive) work for keys too: the backend’s /api/v1/mcp/connections routes resolve a key to its minter (revoking needs an admin key or a grant with destructive). logrite_connection whoami shows a key’s access and workspaceName.

api_keys create takes access (read or full, default full) and ttlDays (30, 90 or 365); create, rotate and revoke stay destructive, which now means an admin key or an OAuth connection granted ‘Delete and revoke’. The backend refuses admin from a credential and a key without expiry — the rules are on API Sandbox.

server_http registers @server.call_tool(validate_input=False); tools/arg_validation.py validates the arguments against the tool’s full schema (registry.all_tools()), the same for every caller, before the scope gate. A failure is Input validation error: <message> with {"error":"invalid_arguments","detail":…} and status 400, through the same path as every other refusal — so it is recorded in the activity trail (ok:false, status:400). Before, the SDK validated before call_tool ran and a call refused for a wrong field name left no row (R7).

It also removes a cross-caller defect (N3): the SDK’s process-wide _tool_cache held the last tools/list, and tools_for narrows schemas per credential, so an OAuth call could be validated against a key’s narrowed enum, or the reverse. An action a key may not run now passes validation and reaches the scope gate, whose refusal says what the key lacks; the narrowed tools/list only decides what a key is shown. A provider apiKey never appears in a validation message. The prompt and resource registrations moved to server_catalogue.py to keep server_http.py under 300 lines; jsonschema>=4 is a direct requirement.

Every client receives instructions at initialize (instructions.py). Without them a model knew LogRite only from tool names, told users AI Warden lived in another workspace, and answered “no compliance logs” while Warden held the evidence (R3). The text:

LogRite is one workspace-scoped platform; everything below lives in THIS workspace and is reachable through these tools.
- Logs and uploads: analytics_query, semantic_search, collector_telemetry. When the user asks LogRite to analyse a log file, use analyse_log_file rather than reading the file yourself.
- Compliance evidence: compliance_logs covers BOTH control-tagged compliance logs and AI Warden's compliance events (HIPAA, SOC 2, AI-judge decisions). An empty compliance-log store does not mean there is no evidence — read its AI Warden section.
- AI Warden: LogRite's gateway for LLM traffic — provider keys (warden_providers), relay keys, captured requests, blocks, compliance events (warden_relay). Rules & Policies (policies) holds the business rules, logging rules and scan profiles that apply to AI traffic, collector logs, AI coding, Cortex chat and code scans. It is part of this workspace, never another one.
- Projects, scans and templates (list_projects, run_compliance_scan, scan_results, project_admin); collector agents (collector_agent); Cortex (cortex_chat for open questions about this workspace's data; knowledge_search for vendor documentation).
Name the workspace by its name and id, as get_workspace_context reports it. Never print a provider API key or a relay key unless the user asked for it.

Tool choice still belongs to the client (G6): the instructions steer it towards analyse_log_file and cortex_chat, they cannot force it.

Three tools, family “AI Warden”. The relay base URL is per estate — https://warden.logrite.ai on prod, https://dev-warden.logrite.ai on dev (config.warden_url_for_headers, ctx.warden_url(), fallback LOGRITE_WARDEN_URL).

warden_providers (new) — the customer’s own LLM provider keys the relay forwards with.

ActionScopeCall and behaviour
list_providerscompliance:readGET /api/v1/warden/providers; says which need baseUrl / apiVersion
list_credentialscompliance:readGET /credentials; rows id, label {provider}-{last4}, provider, baseUrl, status, createdAt
add_credentialsettings:manageprovider required. Without apiKey no backend call is made: the result is connectUrl ({app}/ai-warden?tab=connect) and the prose says to paste the key there (Settings & Keys → 1 · Connect a provider), then call list_credentials. With apiKey: POST /credentials {provider, apiKey, baseUrl?, apiVersion?}; the prose names the credential id and last four, suggests test_credential and warden_relay create_key, and gives the app link. The key is never echoed; a backend 409 passes through
test_credentialsettings:managePOST /credentials/{credentialId}/test; states valid/invalid and the provider’s status
list_modelscompliance:readGET /credentials/{credentialId}/models
delete_credentialdestructivereads the row, then DELETE /credentials/{credentialId}; confirm must equal the label, e.g. openai-ab12

A provider key passed as an argument lives only in the conversation and the request: the activity trail stores argumentsDigest, never arguments, and the backend stores the key encrypted with only its last four in clear.

warden_relay — relay keys (lgw_live_), captured requests, compliance events, stats and suppressions. Handlers live in tools/warden_relay_ops.py and tools/warden_events_ops.py.

ActionScopeCall and behaviour
list_keyscompliance:readper key: name, prefix…last4, status, credential id, policyIds (the business-rules policies reaching it), lastUsedAt
create_keysettings:manage (was destructive)name, providerCredentialId (required), test, includePlaintextKey → POST /relay-keys. Since 2026-10-15 there is no ruleSetIds/complianceProfileId: business-rules policies on AI traffic apply to every key unless narrowed. The prose names the base URL {wardenUrl}/v1/{provider} and says the key goes in Authorization: Bearer. Plaintext only with includePlaintextKey; the AES envelope (relayKeyEncrypted, IV, auth tag, wrapped DEK) is never returned, even then (MCP e11e119)
update_keysettings:managekeyId plus name, status (active or disabled) → PATCH /relay-keys/{keyId}; “changes reach the relay within about a minute”. status:"revoked" is refused: use revoke_key
test_keysettings:managekeyId, model?, message? → POST /relay-keys/{keyId}/test. Prose: Relay key {name} works: {provider}/{model} answered {httpStatus} in {latencyMs} ms; Warden verdict {verdict or "not yet captured"} (request {requestId})., or “the relay blocked the test message: …”, or “the test failed: …”
rotate_key, revoke_keydestructiveunchanged, confirm = the key’s name; “the old key stops working within about a minute”
list_requests, get_request, statscompliance:readunchanged
list_eventscompliance:readGET /compliance-events with hours, fromDate, toDate, framework, rule, q, finalDecision, severity, requestId, relayKeyId, from, limit (≤100). Prose: total, window, up to 5 rows {timestamp} {framework} {rule} → {finalDecision}; nextFrom when more remain
event_statscompliance:readGET /compliance-events/stats?hours= (default 168)
list_suppressionscompliance:readunchanged
suppress_violationcompliance:writeunchanged
unsuppresscompliance:writesuppressionId → DELETE /violations/suppressions/{suppressionId}; “that rule flags again from the next request”
judgecompliance:readGET /api/v1/warden/judge → “AI judge model: … (set by the platform, not per policy)” (moved from warden_rules)

Relay-key reveal is not offered: the backend closes /reveal to every key, and rotate_key hands out a fresh plaintext instead. Minting, editing and testing a relay key are settings:manage by decision of 2026-10-14: a relay key reaches only the relay, never the REST API, and the mint is Maintainer-gated and audited. Rotate and revoke break a running app, so they stay destructive.

policies (replaced warden_rules on 2026-10-15) — Rules & Policies. A key sees the actions its access runs (read: the compliance:read row; full: also compliance:write; admin: also delete). Actions map to /api/v1/policies*:

ActionsScope
list, get, effective, versions, version, export, test, overview, effective_all, activity, changescompliance:read
create, update, set_status, restore, import, enable_framework, disable_framework, ensure_scan_profile, preview_logging_rulecompliance:write
deletedestructive; confirm must equal the policy name

Changes of 2026-10-21 (MCP b7ad9895 … d8b10543, plugin 1.3.8, dev and prod):

  • preview_logging_rule needs compliance:write. The backend always refused it to read keys, yet a read key’s tools/list offered it (MC-1).
  • Four hub reads (all compliance:read): overview → GET /overview; effective_all → GET /effective/all; activity → GET /{id}/activity?days= (policyId required, days 1–30, default 7); changes → GET /activity (page, limit ≤ 100, policyId, actor, from, to) (MC-4).
  • list pages. page (0-based) and limit (default 25, at most 100) map to the backend’s page / size; the answer is {items, total, page, limit, next?}. A page the result budget cuts is cut before next is named, so next resumes at the first row not returned (MC-21, RV-3). Before, list kept 40 of N rows with no way to page.
  • Status words on every credential. The status property’s description reads “enforcing = On, evaluate = Monitor only (business rules only), off = Off” for read, full and admin keys alike (MC-6, RV-4).
  • test lists its targets: collectorId, relayKeyId, projectId (MC-8).
  • update: a caller-supplied revision wins over the fresh read (so a stale edit gets the 409), and description: null clears the description (MC-17).

Body fields follow the PolicyDocument: kind (business_rules / logging_rules / scan_profile), name, status (enforcing / evaluate / off), scope (workspace / projects) with projectIds, features (ai_traffic, logs, ai_coding, cortex, code), relayKeyIds, collectorIds, settings, rules, and revision on update. Frameworks are any libraryKey from list_compliance_frameworks. The old warden_rules actions (create_ruleset, preview_custom, list_agent_rulesets, …) are gone; judge moved to warden_relay.

enable_framework needs libraryKey and features (one or more of ai_traffic, logs, ai_coding, cortex, code; code seeds one scan profile per project in scope) and takes scope?, projectIds? and frameworkAction? (block / notify / record); without features it refuses locally with “features is required for this action — …” and sends nothing upstream (MCP 39d0393). disable_framework takes libraryKey and features?; with none it turns the framework off everywhere.

Upstream timeouts (MCP a18a8e1): every action uses REQUEST_TIMEOUT (10 s) except those that wait on slow backend work. test waits 40 s — the backend’s dry run gives the analyzer’s /internal/scan/evaluate 5 s to connect and 20 s to answer, and under scan load it measured 20.6 s, so at 10 s the MCP used to report a timeout for a dry run that was still working (2 of 4 credential matrix runs). preview_logging_rule, create / update / import of logging_rules, and every restore wait 90 s, because a plain-English logging rule is compiled through the analyzer’s model call (/rules/propose then /rules/validate), which retries a busy model for up to 20 s.

Compliance evidence and queries that cannot silently return nothing

Section titled “Compliance evidence and queries that cannot silently return nothing”
  • compliance_logs reads compliance-logs-<ws> and AI Warden’s relay-compliance-events-<ws> (through the new backend routes) unless source says otherwise. summary adds AI Warden compliance events: N (top 3 frameworks; decisions …), newest …, and when the log store is empty but Warden is not: “The compliance-log store is empty, but AI Warden recorded N compliance events in this window — that is this workspace’s compliance evidence for LLM traffic.” search merges both sources newest first, tags each row source: "logs"|"warden" and reports totals: {logs, warden}. services adds “AI Warden events are per relay key, not per service: use source=warden.”
  • analytics_query finds the index’s real time field. hours used to filter on @timestamp, which relay-compliance-events-* never mapped, so a 90-day window returned 0 (R4). Now analytics_fields.time_fields reads _mapping/field for @timestamp, timestamp, time, flaggedAt, startTime, ts, createdAt and filters on the date-typed ones (one range, or a bool.should over several). A timeField that is not a date → time_field_not_date with the date fields found; no date field at all → no_time_field (”… pass timeField”).
  • A missing index is an error, not 0 hits (R6). The backend proxy marks a translated 404 with X-Logrite-Index-Missing: true; analytics_query turns it into index_not_found (404) with the prose No index named … exists in workspace acme (3103). It has: …. A wildcard that matches 0 hits is checked with _cat/indices/<pattern>; an empty list is the same refusal, while a real empty index stays a real 0. count maps the backend’s 404 the same way. A pattern outside the workspace’s families (zzz-3103-*) is still refused earlier, by ownership.
  • cortex-traces-<ws> is readable, your own turns only (G3). The family joined config.WS_FAMILIES; the backend proxy filters every search on it to the caller’s userId (a key’s minter) and refuses any other read shape — see Multi-tenancy.

The schema gains since, until (ISO-8601) and from (≥0); limit stays ≤100. On the first page (from 0, no until) the tool pins until to now, so every page of one listing shares it and new calls cannot shift the offsets. Rows are fitted to the 32 KB budget before returned is counted. Structured: {rows, total, from, returned, workspaceName, nextPage}, with nextPage: {until, from: from+returned} only while rows remain. Prose: Calls 1–71 of 161 (newest first) in workspace acme (3103); N of these failed. For the next page call get_mcp_activity with until=… and from=71. Before, a busy day was cut at 100 rows and then again by the fit (71 of 100) with no way to reach the rest (R5).

ctx.workspace_label() gives acme (3103) (or the bare id when the name is unknown), and every prose sentence that printed only the id uses it (R9). whoami, list_projects and get_mcp_activity carry workspaceName. get_workspace_context and logrite://workspace/context add a warden section (tools/warden_context.py, from /warden/relay-keys, /warden/stats?hours=168 and /warden/compliance-events/stats?hours=168) and one sentence: AI Warden: N active relay key(s); N request(s), N blocked and N compliance event(s) in the last 7 days., or AI Warden: not set up yet (no relay keys)., or AI Warden: status unavailable (…). when a read fails or cannot be parsed.

Prompts, the Warden skill and the Sandbox catalogue

Section titled “Prompts, the Warden skill and the Sandbox catalogue”

Five prompts joined (10 in all — see Prompts); warden-setup walks list_providers → add_credential (the app link first, “the key stays out of this chat”) → test_credential → a business-rules policy (policies create) → create_key → test_key → point the app at the relay. The plugin skill logrite-warden carries the tool map, that order, the relay URL shape https://warden.logrite.ai/v1/<provider> (dev dev-warden) and “never print keys”; plugin 1.3.6.

scripts/dump-tools.py now writes each tool’s actionScopes (or scope) and, with --merge, the spec’s x-mcp-credential-scopes {read, full, admin}, which the Sandbox Tools panel reads to say what each connection can run (see API Sandbox).