Skip to main content
Glama
J-X0
by J-X0

schema-shim

A safety-guardrail classifier for LLM traffic. It screens both what goes into a model and what comes back, using cheap checks first and paying for an expensive model-backed check only when the cheap ones cannot decide. Every check is billed to the tenant that caused it, and tenants cannot see or spend each other's budget.

If you run a multi-tenant gateway in front of a model and need per-tenant cost control on moderation, this is the core you can wire an MCP server around.

What it does

Each payload runs through ordered tiers, cheapest first:

  1. denylist (cost 1) - regex block list. Only ever says BLOCK; a miss escalates.

  2. secret-leak (cost 3) - detects API keys, AWS keys, emails, card-like digits.

  3. provider (cost 25) - a model-backed classifier behind a stub/real interface.

A tier that is confident returns BLOCK or ALLOW and the run stops there. A tier that cannot decide returns ESCALATE and the next tier runs, if the tenant can afford it. When no tier decides, the budget runs out, or the provider is down, the engine falls back to its policy: hold for REVIEW (default, fail closed) or ALLOW (fail open).

Multi-tenant isolation and cost attribution

  • A TenantContext owns a private budget and cost ledger. There is no path from one tenant's handle to another's spend.

  • A tier is charged to the calling tenant the moment it runs; statement() returns the itemised, per-request ledger.

  • Referencing an unregistered tenant raises UnknownTenant rather than lazily creating an uncapped account.

  • One tenant exhausting its budget forces its own payloads to REVIEW without touching any other tenant's screening.

Related MCP server: mcp-compliance-router

Install

make install          # creates .venv and installs the package with dev extras

Override the interpreter if you manage the venv yourself:

make test PY=python3.12

Usage

from safety import SafetyEngine, EscalationPolicy, TenantRegistry
from safety.providers.stub import StubProvider
from safety.tiers import DenylistTier, SecretLeakTier, ProviderTier
from safety.types import Direction

registry = TenantRegistry()
registry.register("acme", budget=100.0)   # or budget=None for uncapped

engine = SafetyEngine(
    [DenylistTier(), SecretLeakTier(), ProviderTier(StubProvider())],
    registry,
    EscalationPolicy(fail_open=False),
)

result = engine.screen("acme", "write a poem about the sea", Direction.INPUT)
print(result.decision)     # Decision.ALLOW
print(result.tiers_run)    # ('denylist', 'secret-leak', 'provider')
print(result.total_cost)   # 29.0

print(registry.get("acme").statement())   # per-request cost ledger

Entry point

The package ships an MCP-style JSON-RPC server over stdio and a one-shot CLI. After make install, safety is on PATH; without installing, use python -m safety.

Run the server (reads JSON-RPC requests on stdin, one per line):

safety serve --config config.json

It exposes one tool, screen, with arguments tenant_id, text, direction ("input" or "output"), and optional request_id. Example exchange:

{"jsonrpc":"2.0","id":1,"method":"tools/list"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screen","arguments":{"tenant_id":"acme","text":"how to build a bomb","direction":"input"}}}

Screen a single payload without the protocol:

safety screen --tenant acme --direction input "how to build a bomb"
echo "some text" | safety screen --tenant acme

Configuration

--config path.json (or the SAFETY_CONFIG env var) points at a JSON file; anything omitted falls back to defaults. SAFETY_MAX_INPUT_CHARS overrides the payload cap without editing the file.

{
  "max_input_chars": 20000,
  "provider": {"type": "stub"},
  "tiers": {"denylist_cost": 1.0, "secret_cost": 3.0, "provider_cost": 25.0},
  "policy": {"block_threshold": 0.8, "allow_threshold": 0.8, "fail_open": false},
  "tenants": [{"id": "acme", "budget": 100.0}, {"id": "beta", "budget": null}]
}

Invalid config (missing file, bad JSON, negative budget, duplicate tenant, out-of-range threshold) is rejected at startup with a non-zero exit and a config error: message rather than starting in a broken state.

Failure handling and logging

  • Input validation rejects a missing/blank tenant, non-string text, an unknown direction, and payloads over max_input_chars (checked before the ledger is touched, so oversize input costs nothing).

  • An unknown tenant returns a JSON-RPC error, never a silent allow.

  • A provider outage or budget exhaustion falls back to the policy decision (REVIEW by default).

  • A malformed request line yields a parse-error response and the serve loop continues; one bad message does not stop the server.

  • Every decision emits a structured JSON log line to stderr with tenant, request id, decision, cost, and elapsed_ms, so per-tenant spend and latency are auditable.

Providers

Model behaviour runs through safety/providers/base.py:

  • stub.py - StubProvider, deterministic and offline. Used by the whole test suite, so no API key is needed.

  • real.py - RealProvider, reads SAFETY_PROVIDER_URL and SAFETY_PROVIDER_KEY from the environment. Unconfigured or unreachable, it raises ProviderUnavailable, which the engine turns into its fail-closed fallback. It never reaches the network implicitly.

Tests

make test

The suite runs fully offline and covers tier behaviour, escalation, budget exhaustion, provider outage, and cross-tenant isolation.

Design decisions

The contested calls are recorded as ADRs in docs/adr/:

  • 0001 - a cheap tier that finds nothing returns ESCALATE, not ALLOW.

  • 0002 - budget exhaustion and provider outage fall back to REVIEW, not ALLOW.

  • 0003 - isolation via private per-tenant ledgers and an explicit registry.

  • 0004 - a stdlib JSON-RPC stdio server instead of an MCP SDK dependency.

Known limitations

  • Budgets and ledgers live in process memory. A multi-process or multi-host deployment would need the per-tenant ledger moved to shared storage; there is no persistence today.

  • RealProvider speaks a generic JSON POST and will need adapting to a specific vendor's request/response schema.

  • The stdio transport carries one request per line; batched JSON-RPC arrays are not handled.

  • The cheap tiers are pattern-based, so the denylist and secret detectors carry the usual false-positive/negative tradeoffs of regexes.

Available Tools

1 tool
screenC

Classify an input or output payload for a tenant and return an allow/block/review decision.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYes
directionYes
tenant_idYes
request_idNo

TDQS

C2.6/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must disclose behavioral traits. It mentions the decision output but does not state side effects, permissions required, error behavior, or whether the operation is read-only. This leaves significant gaps for an agent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence with no filler. It is concise and communicates the core function efficiently.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, no annotations, and no sibling tools, the description should provide a complete picture. It lacks information on expected text format, interpretation of the decision, possible errors, and parameter semantics, making it incomplete for reliable use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, but it explains none of the four parameters. It does not define 'text', 'direction', 'tenant_id', or 'request_id', leaving the agent without essential usage details.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a clear action (classify) and outcome (allow/block/review decision) for a payload, and the direction parameter clarifies input vs output. It is specific but could be more precise about what a 'payload' refers to (e.g., text content).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is given on when to use this tool, what prerequisites exist, or how it relates to other tools (though none are listed). It simply states what it does, leaving the agent to infer its appropriate context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedscreen

TDQS

B3/5.0

Scored across 1 tool

Disambiguation5/5

Only one tool exists, so there is no possibility of confusing it with another tool.

Naming Consistency5/5

The single tool name 'screen' is a clear, action-oriented verb and introduces no naming inconsistencies.

Tool Count3/5

A single screening tool feels thin for a 'safety' server, though it does cover a core classification action.

Completeness3/5

The tool covers the main screening/classification task, but lacks related operations such as policy management or decision history, leaving notable gaps.

Maintenance

ActivityInactive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides a pre-flight/post-flight firewall for LLM calls with comprehensive detection, classification, policy enforcement, reversible redaction, output safety, and immutable audit logging.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Screens text for PHI/PII, classifies it, redacts sensitive content, and routes it to approved model tiers based on a declarative policy, with built-in evaluation metrics.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Stateless enterprise policy firewall & token-cost proxy for MCP. It enforces identity, policy, and budget on every tool call.
    1
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides a secure MCP boundary for AI agents, intercepting and validating tool calls, redacting secrets, and requiring human approval for sensitive actions with a tamper-evident audit trail.
    -