MCP Server
Overview
Section titled “Overview”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.
| Setting | Value |
|---|---|
| Technology | Python 3.12, Starlette + uvicorn, mcp SDK 1.x |
| Transport | MCP Streamable HTTP (stateless), JSON-RPC 2.0 |
| Port | 9500 |
| Repo | Deployd-io/logrite-mcp-server |
| Prod | https://mcp.logrite.ai |
| Dev | https://dev-mcp.logrite.ai |
Endpoints
Section titled “Endpoints”| Method | Path | Auth | Description |
|---|---|---|---|
POST | /sse | Bearer | Every MCP method: initialize, tools/*, resources/*, prompts/* |
DELETE | /sse | Bearer | Session cleanup. Stateless server, so a no-op |
GET | /sse | — | 405. Stateless: there are no server-to-client streams |
GET | /.well-known/oauth-protected-resource | none | RFC 9728. Named by every 401 from /sse |
GET | /.well-known/oauth-authorization-server | none | RFC 8414. PKCE S256 only, public clients only |
POST | /oauth/register | none | RFC 7591 dynamic client registration. No client secret is ever issued |
GET | /oauth/authorize | none | Browser navigation → <app>/connect/<requestId> |
POST | /oauth/token | none | Authorization-code exchange (PKCE) and refresh |
POST | /oauth/revoke | none | RFC 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-package | Bearer | The 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.sh | none | The one-command Claude Code installer (below) |
POST | /coding/events | lgsi_live_ | The Claude Code plugin’s AI coding session events (below) |
GET | /coding/policy | lgsi_live_ | The workspace’s coding policy for the plugin’s local checks |
GET | /health | none | {"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.
Connecting a client — no key to paste
Section titled “Connecting a client — no key to paste”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.
POST /sseanswers 401 withWWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource".- The client reads that document and
/.well-known/oauth-authorization-server, registers itself atPOST /oauth/register, and opens the browser atGET /oauth/authorize. - 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. - The browser returns to the client’s own callback with a code, exchanged at
POST /oauth/token.
Signing in over SSH (2026-10-09)
Section titled “Signing in over SSH (2026-10-09)”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 acurl -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’ssessionStorage, 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.
The nine scopes
Section titled “The nine scopes”One source of copy, com.logwrite.mcp.McpScopes, read by the consent screen, the tool
refusal and Settings → Connections, so the three cannot drift.
| Scope | Title | What it allows |
|---|---|---|
workspace:read | Read your workspace | See the workspace, its projects, members and enabled add-ons. |
logs:read | Read your logs | Search and read every log index this workspace owns, including collector output. |
logs:write | Send and analyse logs | Analyse log text you provide and send lines through a collector. |
agents:manage | Manage collector agents | Create, configure, pause and rotate collector agents. |
compliance:read | Read compliance | See frameworks, rule libraries, scan results, findings and the maturity assessment. |
compliance:write | Run compliance work | Start repository scans, write rules and policies, and record attestations. |
cortex:use | Ask Cortex | Ask questions about your data and your enabled knowledge packs. |
settings:manage | Change workspace settings | Enable add-ons, configure integrations and observability connections, and set retention. |
destructive | Delete and revoke | Delete 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.
Authentication with a key
Section titled “Authentication with a key”For a client that cannot do OAuth, two credential shapes, chosen by prefix:
- A Logrite API key —
lgp_live_…(or the pre-2026-09lgs_live_…). Introspected against the backend atPOST /api/v1/api-keys/introspectand cached for 60 seconds bysha256(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:
curl -fsSL https://mcp.logrite.ai/install.sh | shDo 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.
401 and 503 are different answers
Section titled “401 and 503 are different answers”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.
Tenant isolation
Section titled “Tenant isolation”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 bycoding_sessions enrol. A missing or unknown token, a revoked one, and anylgp_live_key, session JWT or OAuth token are401 {"error":"unauthorized"}. Thelgsi_live_prefix is deliberately absent fromAPI_KEY_PREFIXES, so the same token is a 401 on/sse: the two planes never share a credential. A backend that cannot be asked is503 backend_unavailable, never a 401. - Token resolve goes to the backend’s
GET /internal/coding/tokens/resolve?tokenHash=(sha256of 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.
POST /coding/events
Section titled “POST /coding/events”Body: NDJSON, gzip (Content-Encoding: gzip, or a body starting with the gzip magic bytes
without the header).
| Condition | Answer |
|---|---|
| Body over 1 MB compressed, over 5 MB inflated, or more than 1000 events | 413 payload_too_large |
| Not valid gzip | 400 bad_request |
coding.enabled off for the workspace | 202 {"accepted":0,"disabled":true} — nothing stored |
| Otherwise | 202 {"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.
GET /coding/policy
Section titled “GET /coding/policy”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.
Rate limits
Section titled “Rate limits”Two groups in rate_limit_groups.py, keyed per token (tokenId) once the token is resolved:
| Group | Route | Default | Override |
|---|---|---|---|
coding-ingest | POST /coding/events | 120 / 60 s | MCP_RATE_LIMIT_CODING_INGEST |
coding-policy | GET /coding/policy | 60 / 60 s | MCP_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.
| Family | Tools |
|---|---|
| Workspace | get_workspace_context, list_projects, get_project, enrol_repository, project_admin, project_members, workspace_members, workspace_addons, workspace_settings, api_keys |
| Cortex | cortex_chat, knowledge_search |
| Logs | analyse_log_file, ingest_log_lines, semantic_search, analytics_query |
| Collectors | collector_agent, collector_telemetry |
| Compliance | list_compliance_frameworks, run_compliance_scan, scan_results, compliance_library, compliance_logs, maturity |
| AI Warden | warden_providers, warden_relay, policies (Rules & Policies; replaced warden_rules on 2026-10-15) |
| Integrations | observability, integrations |
| Audit | get_mcp_activity, governance_audit |
| Connection | logrite_connection |
| AI Coding | coding_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.
What an API key may not run
Section titled “What an API key may not run”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.
Limits and errors
Section titled “Limits and errors”| Case | Answer |
|---|---|
| A tool name the server does not have | Unknown 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 identity | isError: 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 KB | Trimmed, 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:
| Tool | Action | Scope | Calls |
|---|---|---|---|
workspace_settings | get | workspace:read | GET /api/v1/workspaces/{ws}/settings — every setting plus the key catalogue (type, default, bounds) |
set | settings:manage | PATCH 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_sessions | enrol | agents:manage | POST /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, issue | compliance:read | GET /api/v1/coding/summary, /issues, /issues/{id} | |
set_status | compliance:write | PATCH /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.
Worked examples
Section titled “Worked examples”Arguments as a client sends them in tools/call, and the start of what comes back.
# Any tool, by hand — the server speaks JSON-RPC over Streamable HTTPcurl -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":{}}}'| Tool | Arguments | Result (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).
Scans and enrolment
Section titled “Scans and enrolment”run_compliance_scanfinds the project enrolled forrepo(or takesprojectId), mapsframeworksto analyzer templates (soc2,hipaa, … —generalwhen none; see Templates from MCP) and starts a whole-repository scan withPOST /api/v1/scan/initiate. It returns the backend’sRUNNINGanswer and itspollUrl; pollscan_resultswithlatest_runfor the result. It writes nothing to the repository.get_projectreadsGET /api/v1/project/{id}andenrol_repositoryposts toPOST /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.
Templates from MCP
Section titled “Templates from MCP”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:
| Action | Scope | Backend route | What it does |
|---|---|---|---|
list_templates | workspace:read | GET /api/v1/project/{projectId}/templates | The 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_rules | workspace:read | GET /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_templates | compliance:write | POST /api/v1/project/{projectId}/templates | frameworks: 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_template | destructive | DELETE /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_templatesanswersadded,existing(already on the project),unknown(names that resolve to no framework with global rules) and the remainingaddable. When nothing was added and nothing already existed, the result isisErrorwitherror: template_unknown.remove_templateneeds ‘Delete and revoke’. It needs thedestructivescope, which only an OAuth connection granted it or (since 2026-10-14) an admin API key holds; a full or read key’stools/listdoes not offer it and a direct call is refused (scope_not_granted, naming the admin API key). An unknowntemplateIdanswerstemplate_not_found. Removing the last template is allowed; the next scan seedsgeneral.- Who may change templates. Adding and removing need the workspace’s
policy-code-writerole floor (Developer by default; the separatetemplate-writefloor was removed on 2026-10-21, backend7db0d5a1); a Viewer gets 403. Both are written to the governance audit log (project_templates_added,project_template_removed). A template is a scan profile:templateIdis the scan-profile policy id, also accepted bypolicies get/delete(MCP37f82f7d).
A template with chosen rules
Section titled “A template with chosen rules”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.
ruleswithoutname,namewithoutrules, 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_templatesdoes not take (e.g.ruleIds) is refused withunknown_argumentsand 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. Samepolicy-code-writefloor.
Names that are accepted
Section titled “Names that are accepted”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.
What a scan does with a missing framework
Section titled “What a scan does with a missing framework”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).
| Action | Backend route | What it does |
|---|---|---|
approve | GET 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 |
generate | POST /api/v1/generator | scanId + 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
approvepreviews 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 stripsabsFileNameto 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
(
prNumberstays null), solatest_run/get_runreadbranchLinkandrunDetails.countsfromGET /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_scanrefusesmode,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.
Fixes of 2026-10-14
Section titled “Fixes of 2026-10-14”- 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.PerCallerServernow looks the tool up inregistry.tools_for(<this caller>)on every call. Verified live on dev: a session’sgovernance_audit verifycalled 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_customneedstitleas well assourceText(nothing is saved). Since 2026-10-15 this ispolicies preview_logging_rule.compliance_library add_attestationneedsstatus(attestedornot_applicable, sent asATTESTED/NOT_APPLICABLE),owner, anotefornot_applicable, and an attestableobligationIdfromlist_attestations.warden_relay suppress_violationneedsframeworkandrule;requestIdandagentIdonly narrow it.
- Sandbox → MCP lists the current actions. Its index (generated 2026-10-09) predated
list_templates,list_rules,add_templates,remove_template,approveandgenerate; regenerated fromdocs/api-catalog/openapi/mcp.json.
Fixes of 2026-10-14 (second round)
Section titled “Fixes of 2026-10-14 (second round)”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(nowpolicies preview_logging_rule) flags a failed compile ascompile_failed(it used to say “The rule previewed.”), and says “compiled” when it did.warden_relay get_requestreads the{request: …}envelope and names verdict, layer, risk, rules, model and the reason (the judge’s reasoning when that decided it).api_keys introspecton a session or OAuth connection describes that credential instead of relaying the backend’s bare401 {"active": false}.scan_results key_findingson a scan whose AI summary step failed says so and gives the recorded counts, instead of “recorded no key findings”.analytics_query searchprose keeps 300 characters per hit (was 160), enough to show a .NET exception’s status code.governance_audit verifyreports recovered legacy rows, unverifiable rows and forks by id, and never says “all N verified” over any of them.
Resources
Section titled “Resources”| URI | Contents |
|---|---|
logrite://workspace/context | Workspace, role, projects, add-ons, and (since 2026-10-14) AI Warden status |
logrite://compliance/frameworks | The 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.
Prompts
Section titled “Prompts”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.
The Claude Code plugin
Section titled “The Claude Code plugin”📐 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.
The installer
Section titled “The installer”curl -fsSL https://mcp.logrite.ai/install.sh | shThe key comes from LOGRITE_API_KEY when it is set on sh, otherwise from a hidden prompt
on the terminal:
curl -fsSL https://mcp.logrite.ai/install.sh | LOGRITE_API_KEY=lgp_live_… shThe 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 aheadersHelperscript,~/.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, orLOGRITE_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. WhenSSH_CONNECTIONorSSH_TTYis set, the installer says to useclaude mcp login logrite --no-browserinstead (see Signing in over SSH).LOGRITE_AUTH=oauthis 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 happened | What it says |
|---|---|
401 | the key was not accepted — create a new one |
| no answer at all | a 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:
| Variable | Default | Used for |
|---|---|---|
LOGRITE_MCP_URL | https://mcp.logrite.ai | The MCP endpoint registered with Claude Code, and the plugin download |
LOGRITE_BACKEND_URL | https://leapfrogapi.deployd.io | Validating the key (/api/v1/api-keys/introspect) before anything is written |
LOGRITE_APP_URL | https://app.logrite.ai | Where 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.
The zip
Section titled “The zip”Sandbox → MCP downloads logrite-claude-plugin.zip, pre-filled with this workspace’s
frameworks. Install it with:
mkdir -p ~/.logrite && unzip logrite-claude-plugin.zip -d ~/.logrite/claude-marketplaceclaude plugin marketplace add ~/.logrite/claude-marketplaceclaude plugin install logrite@logrite-local -s userThe 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.
The hooks fail open, always (2026-10-07)
Section titled “The hooks fail open, always (2026-10-07)”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:
| Version | Change |
|---|---|
| 1.1.2 | A healed finding is recorded as found, then fixed (fixedInSession: false, then true) |
| 1.1.3 | Stop also scans files changed through Bash — git-modified or untracked files with an mtime at or after the turn’s start |
| 1.1.4 | A 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.5 | A {} 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.0 | Installer 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.1 | The scanner skips the plugin’s own config |
| 1.2.2 | The Stop scan covers what the turn committed |
| 1.3.0 | Multi-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.2 | A fix made in a claude -p --continue turn counts as fixed in session; session_end always ships its spool |
| 1.3.3 | Line 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.4 | Sign-in over SSH: refusals name claude mcp login logrite --no-browser; LOGRITE_AUTH=oauth switches a key install off its key |
| 1.3.5 | The 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.6 | Five new commands and prompts (data-sources, recent-errors, what-happened, warden-setup, warden-report); the logrite-warden skill (2026-10-14) |
| 1.3.7 | The 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.8 | Released 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.
What /logrite:init writes
Section titled “What /logrite:init writes”.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.
Activity trail
Section titled “Activity trail”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 droppedcounter. An audit write that can fail a tool call turns logging into an outage. mcp-activityis a tenant family in bothconfig.pyandcom.logwrite.util.IndexNames. That list is duplicated across two languages and a one-sided edit is silent.compliance-logs(the per-workspacecompliance-logs-<ws>familycompliance_logsreads) is registered on both sides too. Absent fromIndexNames, the OpenSearch proxy’sisTokenAllowedwould treat it as “not a tenant index” and let any workspace read it.- Key lifecycle does not go here.
api_key_minted/rotated/revokedareGovernanceAuditrows 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.
Configuration
Section titled “Configuration”| Variable | Default | Purpose |
|---|---|---|
MCP_PORT | 9500 | Listen port |
SECURITY_JWT_SECRET | — | HMAC key for the JWT path; mirrors the backend’s base64 decode |
LOGRITE_BACKEND_URL | http://backend:8080 | Key introspection, projects, frameworks, enrolment |
LOGRITE_AI_URL | http://ai-service:9300 | Cortex chat, log analysis, knowledge packs |
LOGRITE_SEMANTIC_URL | http://semantic-search:9100 | Hybrid search |
LOGRITE_INGEST_URL | http://ingest-shim:9700 | Collector ingest |
LOGRITE_APP_URL | https://app.logrite.ai | Where 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_URL | https://warden.logrite.ai | The 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_URL | http://localhost:9200 | The cluster |
MCP_API_KEY_CACHE_TTL | 60 | Seconds a verified key stays resolved in-process |
MCP_ACTIVITY_ENABLED | true | The audit trail |
MCP_ACTIVITY_QUEUE_MAX | 1000 | Bounded 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_TTL | 3600 | Access-token lifetime, seconds |
MCP_OAUTH_REFRESH_TTL | 2592000 | Refresh-token lifetime, seconds (30 days) |
MCP_GRANT_CACHE_TTL | 60 | Seconds a resolved grant stays cached; a revoke takes effect within it |
python -m pip install -r requirements-dev.txtpython -m pytest tests -qtests/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.
Acceptance against a LIVE estate
Section titled “Acceptance against a LIVE estate”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:
python3 ops/testing/mcp_acceptance.py --estate prod # ops key, workspace 252LOGRITE_DEV_API_KEY=lgp_live_… \ python3 ops/testing/mcp_acceptance.py --estate dev # workspace 3103python3 ops/testing/mcp_acceptance.py --estate prod --only T7,T23 --jsonExit 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 —
T16beforeT17–T19(the compliance read needs the write that precedes it),T7beforeT23(the trail case asserts the refusalT7just 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.
Close-out changes (2026-10-09)
Section titled “Close-out changes (2026-10-09)”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.”
Error results carry a machine code
Section titled “Error results carry a machine code”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_grantedmanage_url: https://app.logrite.ai/settings?tab=connectionsscope: destructiveThe 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.
analytics_query
Section titled “analytics_query”-
aggregateresolves the field first: a keyword/numeric/date field is used as is; a text field with a.keywordsub-field becomes<field>.keyword(translatedFromsays so); a field that does not exist isunknown_field(with up to 30 fields you can group by); text without.keywordisfield_not_aggregatable. The result is{index, field, translatedFrom, total, buckets:[{key,count}], otherCount}. -
Queries containing
script,script_score,scripted_metric,runtime_mappings,script_fieldsorderivedanywhere 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).searchhit text,aggregatebucket keys and every string insemantic_searchhits go throughtools/pii_mask.py, a port of the backend’sCollectorLogRedactor: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 uploadedssn=123-45-6789came 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
Section titled “Semantic search”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.
Paging and truncation
Section titled “Paging and truncation”list_projectstakeslimit(1–100, default 100) andoffset, returns{items, total, nextOffset}; rows carrybranchCountbut not branch names (get_projecthas them).nextOffsetisoffsetplus the rows actually sent: when the 32 KB budget trims a page, the next page starts at the first row that was cut (mcp-serverd9f0fa8b; before it, rows past the cut were unreachable). The same holds forscan_results workspace_summary.scan_results workspace_summaryprojects each row to the scan’s id, project, branch, language, run date, severity totals and coverage, takeslimit(1–100, default 50) andoffset, 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 andlimit1–100 or the call is refused.
Scan results and runs
Section titled “Scan results and runs”- Numbers name their units. Summary totals are outstanding recommended log statements; details
count finding groups (one per file × logging rule) and also report
statementCountandoutstandingStatementCount. - Summary, details and
get_resultcarry no server paths:checkoutFolderPathis dropped and files are relative (filePath). run_compliance_scan/get_runprose reads the runstate: “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);appliedTemplatesis always filled and the label readsSOC 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.
Workspace context and enrolment
Section titled “Workspace context and enrolment”get_workspace_contextreturnsworkspaceName,mcpUrl,enabledFrameworks— since MCP792c30a0(2026-10-21) only the frameworks whoseenabledFeaturescontainai_coding, which is what/logrite:initwrites; before, every enabled framework was called “enabled for AI coding” — andframeworksByFeature: {feature: [libraryKey…]}. It also carries apoliciessection from/api/v1/policies/overview:{state, byStatus, byKindStatus, attention: [code…], attentionCounts: {code: n}}, each attention code once. Its text startedWorkspace 3103 (logrite-ops).; since 2026-10-14 it readsWorkspace logrite-ops (3103).and adds an AI Warden line.list_compliance_frameworksrows are slim (libraryKey, displayLabel, kind, region, tier, version, ruleCount, enabled, enabledFeatures), so all fit; prose names the enabled ones.rulesno longer requireslibraryKey: without it the backend lists every library (MCP37f82f7d).enrol_repositoryreturnsmcpUrl.knowledge_searchanswers “No entry in the enabled packs covers that.” withmatched:falsewhen no topic matches, instead of an unrelated topic.
Collectors and others
Section titled “Collectors and others”collector_telemetry:hoursis 1–8760; onlylogsandflagsaccept0(all time). An argument the action does not take, orfromlater thanto, is refused. Log/flag prose states whetherrevealwas honoured (“reveal was requested but NOT honoured …” for API keys).collector_agentcollectPath: 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_addonschecksaddonKeyagainst^[a-z0-9][a-z0-9-]{0,63}$.integrations salesforce_casessays when no Salesforce org is connected.api_keys introspectand other prose never print “None”.workspace_settingslists and accepts the privacy keys (warden.redaction.*,cortex.redaction*,warden.sensitiveRawCapture) next toingest.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.
Close-out fix wave 2 (2026-10-09)
Section titled “Close-out fix wave 2 (2026-10-09)”- Templates are visible (K13).
list_projectsrows andget_projectcarrytemplatesas objects withname,logTypeandkey. Template names are accepted in several spellings: for examplePCI DSS,pci_compliance,pciandPCIDSSall resolve to the same template. - A single framework is seeded (AJ-15).
run_compliance_scanwith one framework sends it astemplate, 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_searchsays what it searched (K14). A no-match reply reads “No entry in the enabled packs covers that.” and its structuredContent carriespacksSearched(the packs actually searched, never empty when packs were consulted) anddisabledAddons(add-ons that would cover the question but are not enabled). The prose names a disabled add-on when one matches.api_keysfor API keys. With an API key the tool’s description says onlyintrospectis available and why.- Revoked grants stop in seconds. The OAuth grant cache TTL is 3 s, and a backend 401
grant_revokedevicts the grant at once; the tool result isisErrorending witherror: grant_revoked. - Scan details state their scope.
scan_results detailsreportsdetailsScope, 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.
What an API key can run (2026-10-14)
Section titled “What an API key can run (2026-10-14)”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):
| Access | Scopes | Minted by |
|---|---|---|
read | workspace:read, logs:read, compliance:read, cortex:use (READ_SCOPES) | an Owner/Admin in the app, or a credential (below) |
full (default) | every scope except destructive | the same |
admin | all nine, including destructive | only 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:
| Key | Text |
|---|---|
full, destructive needed | That 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. |
| read | This 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.
Arguments are validated inside call_tool
Section titled “Arguments are validated inside call_tool”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.
Server instructions
Section titled “Server instructions”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.
The AI Warden tools
Section titled “The AI Warden tools”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.
| Action | Scope | Call and behaviour |
|---|---|---|
list_providers | compliance:read | GET /api/v1/warden/providers; says which need baseUrl / apiVersion |
list_credentials | compliance:read | GET /credentials; rows id, label {provider}-{last4}, provider, baseUrl, status, createdAt |
add_credential | settings:manage | provider 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_credential | settings:manage | POST /credentials/{credentialId}/test; states valid/invalid and the provider’s status |
list_models | compliance:read | GET /credentials/{credentialId}/models |
delete_credential | destructive | reads 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.
| Action | Scope | Call and behaviour |
|---|---|---|
list_keys | compliance:read | per key: name, prefix…last4, status, credential id, policyIds (the business-rules policies reaching it), lastUsedAt |
create_key | settings: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_key | settings:manage | keyId 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_key | settings:manage | keyId, 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_key | destructive | unchanged, confirm = the key’s name; “the old key stops working within about a minute” |
list_requests, get_request, stats | compliance:read | unchanged |
list_events | compliance:read | GET /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_stats | compliance:read | GET /compliance-events/stats?hours= (default 168) |
list_suppressions | compliance:read | unchanged |
suppress_violation | compliance:write | unchanged |
unsuppress | compliance:write | suppressionId → DELETE /violations/suppressions/{suppressionId}; “that rule flags again from the next request” |
judge | compliance:read | GET /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*:
| Actions | Scope |
|---|---|
list, get, effective, versions, version, export, test, overview, effective_all, activity, changes | compliance:read |
create, update, set_status, restore, import, enable_framework, disable_framework, ensure_scan_profile, preview_logging_rule | compliance:write |
delete | destructive; confirm must equal the policy name |
Changes of 2026-10-21 (MCP b7ad9895 … d8b10543, plugin 1.3.8, dev and prod):
preview_logging_ruleneedscompliance:write. The backend always refused it to read keys, yet a read key’stools/listoffered it (MC-1).- Four hub reads (all
compliance:read):overview→GET /overview;effective_all→GET /effective/all;activity→GET /{id}/activity?days=(policyIdrequired,days1–30, default 7);changes→GET /activity(page,limit≤ 100,policyId,actor,from,to) (MC-4). listpages.page(0-based) andlimit(default 25, at most 100) map to the backend’spage/size; the answer is{items, total, page, limit, next?}. A page the result budget cuts is cut beforenextis named, sonextresumes at the first row not returned (MC-21, RV-3). Before,listkept 40 of N rows with no way to page.- Status words on every credential. The
statusproperty’s description reads “enforcing = On, evaluate = Monitor only (business rules only), off = Off” for read, full and admin keys alike (MC-6, RV-4). testlists its targets:collectorId,relayKeyId,projectId(MC-8).update: a caller-suppliedrevisionwins over the fresh read (so a stale edit gets the 409), anddescription: nullclears 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_logsreadscompliance-logs-<ws>and AI Warden’srelay-compliance-events-<ws>(through the new backend routes) unlesssourcesays otherwise.summaryaddsAI 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.”searchmerges both sources newest first, tags each rowsource: "logs"|"warden"and reportstotals: {logs, warden}.servicesadds “AI Warden events are per relay key, not per service: use source=warden.”analytics_queryfinds the index’s real time field.hoursused to filter on@timestamp, whichrelay-compliance-events-*never mapped, so a 90-day window returned 0 (R4). Nowanalytics_fields.time_fieldsreads_mapping/fieldfor@timestamp, timestamp, time, flaggedAt, startTime, ts, createdAtand filters on the date-typed ones (onerange, or abool.shouldover several). AtimeFieldthat is not a date →time_field_not_datewith 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_queryturns it intoindex_not_found(404) with the proseNo 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.countmaps 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 joinedconfig.WS_FAMILIES; the backend proxy filters every search on it to the caller’suserId(a key’s minter) and refuses any other read shape — see Multi-tenancy.
get_mcp_activity pages through a busy day
Section titled “get_mcp_activity pages through a busy day”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).
The workspace by name
Section titled “The workspace by name”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).