How it works
Defenses
Attack Patterns MCP Attack Atlas What we catch Hardening manual OWASP LLM Top 10 MITRE ATLAS
Learn
Encyclopedia (new) Agent Security 101 Blog Reports CVP runs Thesis
Resources
Docs GitHub Action (live) vs Lakera vs Promptfoo Team
Theme
0.6.0 beta

The MCP proxy

Measured on main at ec753bb, shipped in 0.6.0 on 2026-09-26
Read this first

Sunglasses runs your MCP server as a child process and sits between it and your client. It enforces only once a person has approved that server's tool snapshot at an interactive terminal. Until then nothing is forwarded and nothing is inspected, and installing it is not protection.

Two states, and only one of them protects anything

The proxy has a before and an after, and they are not degrees of the same thing. Before approval it refuses every call and scans nothing. After approval it inspects both directions and withholds a message the engine blocks. Every sentence on this page belongs to one state or the other, and says which.

sunglasses://proxy/two-states
StateWhat your client getsWhat is inspected
Wrapped, not approvedEvery tools/list and tools/call comes back as a typed refusal, APPROVAL_REQUIRED. initialize is answered normally.Nothing. status: not_run, inspected_utf8_bytes: 0 — the call was never forwarded, so the server was never even asked.
Approved at a terminalOrdinary traffic passes and returns its real result. A message carrying a credential comes back as SUNGLASSES_WITHHELD with the reason, the rule, the bytes inspected and the rule ids.Both directions, in the credential lane — what your client sends, and what the server returns.

The refusal is the honest part. A gate that opened itself would be a gate you never agreed to, so the first state is the default and it is reached by every client that is not a person at a keyboard.

1) Install — it edits your config, it never creates one

sunglasses install <name> rewrites one entry in an MCP config so that server launches through Sunglasses instead of directly, and records enough on disk to put it back exactly. Aimed at a path that does not exist it refuses and names the file, because a client's server list is that client's file and a guessed one would sit where the client is not looking.

sunglasses://proxy/install
The command

sunglasses install github — edits ./.mcp.json by default. --config <path> names a different one.

What it writes

The entry's command becomes your interpreter running -m sunglasses.proxy, with your original server command after --. It also stores the artifact path and its SHA-256 alongside, so the wrapping can be checked later rather than assumed.

No config, no guess

SUNGLASSES install failed — cannot read /path/to/.mcp.json: [Errno 2] No such file or directory. It names the target and changes nothing.

Putting it back

sunglasses uninstall github reads the record, checks the copy still matches the digest taken at install time, and restores it byte for byte. If the record or the copy is not something it can vouch for, it refuses rather than writing bytes it cannot verify.

What it is not

Wrapping is not approval. The command exits 0 and your entry is wrapped, and at that moment nothing is being inspected.

2) Approve — a person, at a terminal, for one server

Start the server once from your client. The first tool listing writes a snapshot of the server's tool descriptors, and the refusal your client receives carries the two values you need to approve it — server_id and snapshot_sha256, in the error payload itself, so you never go hunting in a state directory.

sunglasses://proxy/approve
The command

sunglasses proxy approve <server_id> --snapshot <snapshot_sha256>, run at a real terminal. It shows you the tool descriptors and asks.

A pipe cannot approve

Run it without a terminal and it exits 1, unrecorded: approving records that a human viewed this capture, and this is not an interactive terminal, so nobody did. Answering no at a real prompt also exits 1 and records nothing.

One approval, one server

The snapshot hash covers the descriptors, so two different servers exposing the same tools carry the same snapshot_sha256. The approval is stored against the server_id, which differs. Approving one leaves the other refusing until it is approved on its own.

Why that matters

Changing the command behind a familiar tool list does not inherit the approval you already gave. A server that presents the same descriptors as one you trust is still a server you have not approved.

Where it lives

The install record, the capture, the approval and the receipts all sit under one state root, ~/.sunglasses/proxy/.

3) What is blocked, once approved

With the snapshot approved the mediator inspects messages in both directions and withholds one whose content the engine blocks, returning a typed JSON-RPC error rather than a silent drop. The reason code, the rule, the bytes it inspected and the rule ids that fired are all in the payload.

sunglasses://proxy/enforcement
DirectionWhat happensReason code
Your client → the serverA credential in a tool call does not reach the server. Verified by reading the receiving server's own input, not by asking the proxy.PROHIBITED_SECRET
The server → your clientA credential in a tool result is withheld from your client. The -API rule ids are the tool-result channel, so the direction is readable from the payload.PROHIBITED_CONTENT
Ordinary traffictools/list returns the real list and a benign call returns its real result.—

The last row is not filler. Without it, the two above would be equally satisfied by something that refused every message it was given.

What this does not claim

What ships is the credential lane, not general inspection of everything a tool returns. The honest boundary is a property of one rule's anchor, and it is wider than a single channel.

sunglasses://proxy/not-claimed
Not general inspection

The credential lane covers the known credential formats. Naming a tool result as somewhere an attack arrives is a description of the threat, not a claim about what is inspected on the way back.

GLS-SD-010

It is line-anchored, and that is a limit in every channel. It matches an assignment at the start of a line, so a KEY=value sitting inside a JSON string, behind a quote, or simply indented is not matched on any channel — not in a tool result, and not in a file or a message either, which are channels it does declare.

What is still caught

When the value is in a known credential format the GLS-SD-001 family still catches it everywhere — GLS-SD-001 on file, message and web content, GLS-SD-001-API in a tool result. What is actually uncovered is an assignment whose value has no recognisable shape: a password, a DSN, an internal token, once it is embedded.

Closing it

Needs a different anchor, which is a new rule with its own fixtures rather than a channel added to this one.

No comparison

Nothing here is a claim about any other tool.