New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

orangerail-mcp

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

orangerail-mcp

Serve an ontology registry as an MCP server: typed read tools, destructive writes staged for human approval, and an audit log of every call.

latest
Source
npmnpm
Version
0.1.5
Version published
Maintainers
1
Created
Source

orangerail-mcp

Generate a governed MCP server from an ontology registry. This is the only orangerail package that depends on @modelcontextprotocol/sdk (NOLLM-01 scope rule).

createMcpServer({ registry, store, resolveIdentity?, preset?, redactAudit?, reportFailure?, allowDevMode?, hostApprovalPrompt? }) returns { server, serve } and uses the low-level Server with explicit tools/list + tools/call handlers (not McpServer.registerTool, not the experimental tasks API), so input validation stays in exactly one place — the core engine.

Tools

  • <object>_get / <object>_list — one per object with a resolve contract. Objects with readAccess: 'authenticated' deny anonymous callers.
  • <actionName> — one per action. A policy-gated action stages and returns structured { status: 'approval_pending', approvalId }; the agent must poll check_approval to complete it.
  • check_approval — the re-check surface. pending / rejected report status; an approved approval is executed in this server process and the result returned; consumed reports consumed (idempotent re-poll). Under the sandbox preset it records a dry_run instead — see Presets.

Tool names are validated against ^[a-zA-Z0-9_-]{1,64}$ and checked for collisions at build time (fail fast).

Presets

  • approval-for-writes (default) — actions exposed as declared.
  • sandbox — engine dry_run mode; actions return dry_run, never execute. This covers check_approval too: the engine refuses before the consume CAS, so a sandbox server sharing a store with a live one neither executes an approval the live server staged nor burns it — the approval stays approved and the live engine still completes it. The destructive tool stays visible and simply cannot cause an effect, which is the same design as an approval gate.
  • readonly — no action tools, no check_approval.

The host's own approval prompt (hostApprovalPrompt)

Optional, 'off' by default. When set, selected action tools carry _meta: { "anthropic/requiresUserInteraction": true } in tools/list, which asks the host to run its own permission prompt on every call to them.

valueannotated tools
'off' (default)none — the listing carries no _meta at all
'ungoverned-actions'actions with no policy: { approval: 'required' }
'all-actions'every action tool, governed and ungoverned

Read tools and check_approval are never annotated, under any value. check_approval is polled in a loop until a human decides — a prompt on every poll is unusable, and a host mode that never prompts denies a flagged call rather than asking, which would break completion outright. readonly therefore emits nothing in any mode: it exposes no action tools to annotate. sandbox still annotates, because the value describes what the declaration says rather than what the current preset does with it.

Two things to be exact about.

It is enforced by the client, so it is not what makes the gate hold. A governed action stages and waits for a human whether the host prompts, does not prompt, or ignores the key entirely. This is a second checkpoint on top of the rail. The tools it is for are the ungoverned ones, which have no rail in front of them at all.

A flagged tool's prompt cannot be turned off by the person answering it. Per the Claude Code documentation the prompt appears in every permission mode including bypassPermissions, offers no "don't ask again", and is not skipped by an allow rule; in dontAsk mode the call is denied instead, and under --permission-prompt-tool an allow result is converted to a deny. So turning this on can stop a headless pipeline that was working, which is why it is opt-in rather than a default.

Claude Code v2.1.199+ is the only host known to honor the key. It is vendor-prefixed per the MCP _meta key-name rules, so other hosts see metadata they do not recognize and ignore it.

Identity and dev mode

Stdio callers are resolved with resolveCaller({ transport: 'stdio' }). With no resolveIdentity adapter, the local server runs in dev mode (allowDevMode: true): the synthetic local-dev identity is used and every audit record it produces is stamped devMode: true. Remote transports (post-v0) MUST pass allowDevMode: false.

Error redaction

A datasource error is never forwarded to the agent. orangerail sits between the agent and the datasource precisely so that surface is controlled, and a raw Prisma/driver message names tables, constraint names, query text, and absolute file paths.

Every failing path — a throwing action execute, a throwing read resolver, a blocked audit append, any other throw out of tools/call — returns:

{
  "status": "failed",
  "correlationId": "6f1d0d2e-…"
}

with a message naming the tool, the domain-level cause, the kind of error being withheld, and where an operator can find it:

Tool "update_order" failed: the datasource rejected the action. The datasource error is withheld; an operator can read it in the audit log or host log under correlationId "6f1d0d2e-…".

The agent keeps enough to decide whether to retry, choose another tool, or escalate — it just never learns the schema.

The message is held to the same honesty standard as the rest of the project: it says what KIND of error was withheld (a store error for audit_blocked, not a datasource one) and names only a channel that actually holds the text:

statuscausewithheldwhere
failedthe datasource rejected the actiondatasource erroraudit log or host log
resolve_errorthe target could not be read from the datasourcedatasource erroraudit log or host log¹
audit_blockedthe audit record could not be written, so nothing ranstore errorhost log
internal_erroran unexpected internal errorunderlying errorhost log

¹ a resolve_error raised by a read tool says host log only — reads are not audited by design, and pointing an operator at a record that cannot exist is the same class of error as leaking one.

Classified configuration failures

Redaction is absolute about the datasource's own words, and that used to make orangerail's own first-run diagnostics disappear along with them: an agent whose project simply had no DATABASE_URL was told "the datasource rejected the action", which it cannot act on.

So a failure orangerail can positively identify carries a classification rather than a message. The failing layer attaches a code from a closed set; this package owns the sentence for each code:

diagnosticwhat actually happened
datasource_client_missing@prisma/client is not installed or not generated
datasource_model_missingthe client was generated from a different schema
datasource_not_configuredthe connection URL is missing or unusable
{
  "status": "failed",
  "diagnostic": "datasource_not_configured",
  "correlationId": "6f1d0d2e-…"
}

Tool "createNote" failed: the datasource is not configured, so the client could not connect. Its connection URL is missing or unusable — for a Prisma project that is the DATABASE_URL environment variable, which must be set for the process running the orangerail server. Set it, then retry. The datasource error is withheld; an operator can read it in the audit log or host log under correlationId "6f1d0d2e-…".

This is not a hole in the redaction, and it is worth being precise about why. The channel carries no string from the failing layer: a code from a closed enum, plus an optional subject that must match /^[A-Za-z_][A-Za-z0-9_]{0,63}$/ and is dropped otherwise. The prose lives here, in the transport. A datasource that forged the marker could therefore achieve exactly one thing — making orangerail print one of orangerail's own sentences. The driver text is still withheld, and the message still says so. Anything orangerail cannot classify carries no diagnostic at all and is redacted exactly as before.

The FULL text goes to the operator instead:

  • reportFailure — runs on every failure path, so the host log always has it. Defaults to stderr (on stdio, stdout is the JSON-RPC channel). Pass your own sink to route it into a host logger: reportFailure: ({ status, tool, correlationId, error }) => log.error(…).
  • the audit record — engine-raised failed / resolve_error records carry the full error, keyed by the same correlationId the agent was handed (approvalId on the approval path).

Caveats

  • Approved-but-never-repolled: v0 has no approval expiry. An approved approval the agent never re-polls via check_approval simply never executes — it sits approved in the store indefinitely (visible via orangerail approvals).
  • Plaintext storage: staged inputs, results, and errors are stored in plaintext by default (see orangerail-core). Supply redactAudit to mask audit records; do not put secrets in action inputs. The audit log is an operator artifact — it holds the unredacted failure text on purpose. An execution_started record also carries the target's prior state; mask it with redactPrior, which is a separate hook because a row is not an input (a redactAudit alone withholds the row rather than half-masking it).
  • Config is code: loading an ontology config is arbitrary code execution (same trust level as an npm script); v0 is stdio only, no network exposure.
  • The audit log is an audit trail behind a human checkpoint, not a tamper-evident boundary. verifyAudit cross-checks the chain against the approvals store, but the chain hash is unkeyed and its anchor is unsigned and sits in the same directory, so an attacker who can write the store can rewrite both logs consistently. Put the store somewhere the governed agent cannot write — orangerail init scaffolds it inside the scanned project by default. Stated exactly under What the audit log proves.

Keywords

mcp

FAQs

Package last updated on 06 Aug 2026

Related posts