@sigilcore/mcp-proxy
v0.5.0
Published
Wraps MCP tool connections in Sigil Intent Attestations
Readme
@sigilcore/mcp-proxy
Wraps MCP tool connections in Sigil Intent Attestations. Every tool call is evaluated against your operator's warranty.md policy before it reaches the real MCP server.
MCP action taxonomy and trust
Governed MCP calls submit the namespaced action mcp.<serverId>.<toolName>. The
proxy keeps the binding identity in metadata.serverId, the tool name in
metadata.toolName, and the complete arguments in metadata.arguments. Sign
matches policy against those exact metadata values. It never splits the action
string, because server IDs may contain dots, slashes, URLs, or scoped package
names.
Use a ## mcp block to allowlist servers and tools, block tool patterns, or
place a tool behind a durable approval hold. A trailing * is the only wildcard
form. Without a ## mcp block, every MCP action is denied.
The proxy always calls @sigilcore/agent-hooks with failMode: "closed".
--unsafe-bypass remains an explicit emergency escape hatch and logs every
ungoverned call. A normal client-side npm installation is not a trusted shim:
its API key shares the agent environment. Only a controlled deployment with a
dedicated credential inaccessible to the governed agent can satisfy a policy's
require_shim: true or attested rule.
For PENDING, the proxy polls the authenticated tenant-scoped hold-status
endpoint. The legacy APPROVED input alias and canonical ALLOWED status do
not authorize the MCP call by themselves. The proxy submits the unchanged
canonical intent to Sign again. It forwards the tool call only when that
exact-intent retry produces an authorization capability. Denial, expiry,
timeout, changed intent, consumed approval, a missing attestation, or any
unexpected response fails closed.
Policy 2.2 result inspection
An operator can enable the Policy 2.2 response-disclosure path with an opaque
inspection profile. The profile is not sufficient by itself: first pin the
matching local verification context in sigil.config.json (replace every
angle-bracket value with the exact values for the signed policy):
{
"inspectionProfile": "operator-local-v1",
"responsePolicyVerification": {
"profile": "operator-local-v1",
"publicKeyBase64url": "<operator-ed25519-public-key>",
"issuer": "<issuer>",
"keyId": "<key-id>",
"tenantId": "<tenant-id>",
"taskId": "<task-id>",
"policyHash": "<lowercase-sha256>",
"revocationEpoch": 0,
"deterministicRulesetDigest": "<lowercase-sha256>",
"classCatalogDigest": "<lowercase-sha256>"
}
}Then run the proxy with the same profile:
npx @sigilcore/mcp-proxy --inspection-profile operator-local-v1 -- npx @some/mcp-serverFor an explicitly covered MCP tools/call, the proxy constructs the complete
coverage token from its trusted serverId, one literal dot, and the exact SDK
tool name. It compares the whole token byte for byte against the verified
signed policy. It does not split, normalize, trim, case-fold, or infer a tool
category.
The upstream tool runs at most once. Its decoded result is projected and
evaluated locally. The original result is returned only after one bound
ALLOW decision and one forward claim. BLOCK, malformed policy, coverage or
digest mismatch, unsupported binary or mixed content, limit failure,
cancellation, timeout, and evaluator failure return a stable proxy error
without upstream content. When the tool completed, the error states that
execution completed and result disclosure was denied.
Inspection profiles are mutually exclusive with --unsafe-bypass. They also
refuse resources/read, prompts/get, subscriptions, and unknown methods,
because those result types are outside Policy 2.2 coverage. Raw result bytes
remain inside mcp-proxy and the local Agent Hooks evaluator. They are not sent
to Sign or written to logs, metrics, state records, or error bodies.
Policy 2.3 durable scanner enforcement
Policy 2.3 adds operator-owned scanning, verified redaction, and durable
multi-replica disclosure state. It is available through the startProxy
library API. The command-line wrapper intentionally has no scanner endpoint or
database credential surface: an operator host must inject both boundaries.
Construct PostgresExecutionLedger with a query client implementing the narrow
PostgresQueryClient interface. A pg.Pool or pg.Client satisfies that
structural interface. Construct the scanner client with an authenticated
AuthenticatedScannerTransport from Agent Hooks, then pass both to
startProxy as executionLedger and scannerClient.
Every Policy 2.3 tools/call must also carry a caller-issued invocation key in
params._meta["io.sigil/idempotency-key"]. The key is an opaque 1–128 character
ASCII token beginning with an alphanumeric character. Reuse it for transport
retries of one invocation and generate a new value for each deliberate call,
including repeated calls with identical tool arguments. The proxy hashes the
key into its durable execution identity, never logs or persists the raw value,
and blocks before upstream execution when the key is absent or malformed.
The PostgreSQL adapter initializes sigil_execution_ledger and stores only
execution metadata, state, timestamps, and SHA-256 bindings. It never stores a
request body, response body, projection bytes, scanner body, or finding
content. Create and transition operations use compare-and-swap updates. A
forward claim succeeds once only, including under multi-replica contention.
After restart, reconcileStranded terminally blocks abandoned work and never
re-executes the upstream tool. purgeExpired respects legalHold metadata.
Database unavailability blocks before upstream execution.
Scanner projection bytes cross only the injected authenticated operator
transport. Required scanner authentication, deadline, transport, schema,
binding, size, class, confidence, offset, or evidence failures block result
disclosure. Optional scanner failure preserves the signed optional behavior.
Disposition precedence is BLOCK, then REDACT, then ALLOW; observe-only
findings do not change disposition. Verified redactions must map wholly to
UTF-8 boundaries inside one projection record. The proxy applies them to a
clone of the decoded MCP result, preserves the result shape, binds the
disclosed-result digest in the atomic forward claim, and fails closed on any
mapping or shape ambiguity.
Quick Start
# Install
npm install -g @sigilcore/mcp-proxy
# Set your API key
export SIGIL_API_KEY=sk_sigil_YOUR_KEY
# Generate sigil.config.json, then replace the expectedPolicyHash placeholder
# with the lowercase SHA-256 hash of the policy this proxy must enforce
mcp-proxy --init
# Wrap any MCP server after the policy hash is pinned
mcp-proxy -- npx @some/mcp-serverThe generated placeholder is intentionally not runnable. Default enforcement
fails startup until decisionVerification.expectedPolicyHash contains the
exact lowercase policy hash.
MCP Client Config
Change one line in your MCP client config:
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["@sigilcore/mcp-proxy", "--", "npx", "@some/pg-mcp"]
}
}
}Configuration
Generate a starter config: npx @sigilcore/mcp-proxy --init
Precedence: CLI flags > environment variables > config file > defaults.
| CLI Flag | Env Var | Config Key | Default |
|---|---|---|---|
| --key | SIGIL_API_KEY | (env only) | (required) |
| --sign-url | SIGIL_SIGN_URL | signUrl | https://sign.sigilcore.com |
| --log-level | SIGIL_LOG_LEVEL | logLevel | info |
| --server-id | — | serverId | derived from command/URL |
| --server-name | — | serverName | same as serverId |
| --pending-timeout | SIGIL_PENDING_TIMEOUT | pendingTimeout | 30000 |
| --unsafe-bypass | (CLI only) | (CLI only) | false |
| --inspection-profile | SIGIL_RESPONSE_INSPECTION_PROFILE | inspectionProfile | disabled |
| --max-in-flight-results | SIGIL_MAX_IN_FLIGHT_RESULTS | maxInFlightResults | 16 |
| — | — | decisionVerification | { "mode": "enforce" } |
| --remote | — | — | — |
| --port | — | — | auto |
signUrl must always be an exact canonical HTTPS origin. The proxy rejects
HTTP, paths, credentials, query strings, and fragments before it opens either
the Sign connection or the wrapped MCP connection.
Decision verification
Signed decision verification defaults to enforce. Startup fails before any
network connection unless configuration supplies a lowercase SHA-256 policy
hash. An unsigned or unverifiable success response cannot reach the wrapped MCP
server. The proxy accepts APPROVED only as a deprecated input alias and emits
ALLOWED in decision logs.
CI also runs a fail-closed type-assertion gate across all runtime source. It
rejects every non-const as assertion and every angle-bracket assertion,
independent of aliases, conditional types, mapped types, or container shape.
Only TypeScript's literal-narrowing as const form is allowed; it cannot mint
or coerce an authorization capability and the production typecheck remains
mandatory.
This prevents syntactic wrappers from forging the capability required by the
governed-forward helper.
The proxy obtains verification keys from the configured Sign origin's bounded
multi-key JWKS. Agent Hooks caches the complete overlap set for five minutes
and refreshes once when a new key ID appears. A cold-cache JWKS outage denies
the request with key_unavailable. A warm cache continues to verify every key
in the overlap set during an outage. This permits a Sign key rotation without a
single-key client lock.
An optional pinnedJwk takes precedence over JWKS. Use it only to hold one
known-good key during an incident. A static single-key pin does not follow a
normal Sign rotation automatically.
Enforce startup validates the policy hash, optional public JWK, and canonical HTTPS Sign origin before it opens either network connection:
{
"decisionVerification": {
"mode": "enforce",
"expectedPolicyHash": "<lowercase-sha256>",
"pinnedJwk": {
"kty": "OKP",
"crv": "Ed25519",
"kid": "<key-id>",
"x": "<base64url-public-key>",
"use": "sig",
"key_ops": ["verify"],
"alg": "EdDSA"
},
"attestationIssuer": "sigil-core"
}
}pinnedJwk and attestationIssuer are optional. expectedPolicyHash is
required. To restore the compatibility path during an availability incident,
set mode to warn explicitly and roll back the package under the approved
runbook. Warn mode still verifies and reports signed responses, but it permits a
legacy unsigned success response.
Release verification runs a deterministic 29-case enforce-mode batch and named clock-skew, cold-cache outage, rotation-overlap, oversize, and tamper drills. The batch fails on any unexpected valid-case verification failure, accepted tamper, or legacy-path fallback.
Server Identity
serverId— Binding identity (security-critical). Used in txCommit preimage and policy evaluation. Auto-derived from the package name (stdio) or full URL (HTTP) if not set.serverName— Display label (logs only). Defaults to serverId.
Unsafe Bypass
The proxy is fail-closed by default. When no HTTP response is received from Sign, tool calls are blocked.
To allow ungoverned calls during Sign outages, use the --unsafe-bypass CLI flag. This flag is intentionally not available as an env var or config file option — it must be visible in your MCP client config:
npx @sigilcore/mcp-proxy --unsafe-bypass -- npx @some/mcp-serverEvery bypassed call emits an ungoverned_tool_call error-level log. Only an
explicit in-memory transport fail-open capability minted by Agent Hooks after a
no-response request can activate bypass. failOpen and SIGIL_UNREACHABLE
strings from a response are not trusted. Reached redirects, malformed,
unverifiable, 429, and 5xx responses, arbitrary authorization exceptions, and
auth failures (401/403) are never bypassed.
Startup fails if an inspection profile is also configured.
HTTP/SSE Transport
Proxy a remote MCP server:
npx @sigilcore/mcp-proxy --remote https://api.example.com/mcpUpstream Authentication
Remote MCP servers often require auth. Configure upstream headers in your config file using environment variable references:
{
"upstream": {
"headers": {
"Authorization": "Bearer $UPSTREAM_TOKEN",
"X-Custom-Header": "$CUSTOM_HEADER_VALUE"
}
}
}All header values must reference at least one $IDENTIFIER env var. Raw secrets are rejected at load time.
Convenience shortcut for Authorization header:
export SIGIL_UPSTREAM_AUTH="Bearer sk-abc123..."
npx @sigilcore/mcp-proxy --remote https://api.example.com/mcpExtractors
Map tool arguments to Sigil policy fields in sigil.config.json:
{
"extractors": {
"fetch": { "url": "url" },
"write_file": { "path": "path" }
}
}Error Codes
-32001— Sigil policy denial (DENIED, fail-closed, hold timeout)-32002— Sigil authentication failure (invalid API key)-32004— method refused because its result is outside the active inspection profile
License
MIT
