io.github.giancarloerra/januscope

Local MCP proxy for tool restrictions, response redaction, audit logs, and database schema context.

DatabasesTypeScriptv0.5.0

JanuScope

"Whatever you see anywhere (sky, sea, clouds, lands) are all closed and opened by my hand." Ovid, Fasti I, 117–18 (Janus speaking)

JanuScope is the local MCP policy proxy. One YAML wraps any MCP server with policy, redaction, audit, and database-schema injection. JanuScope runs locally, with no hosted gateway in the data path. Your upstream server and model provider can still receive data.

JanuScope hides the dangerous tools, scrubs matching PII out of returned values before the model reads them, records call outcomes, and pre-injects your DB schema to reduce discovery calls. Runs locally, no hosted gateway in the data path.

One YAML (called a Lens) wraps any MCP server with security guardrails, schema injection, and full audit logging. There are 20 bundled Lenses covering databases (Postgres, MySQL, MongoDB, ClickHouse, Redis, SQLite, Microsoft SQL Server / Azure SQL, Oracle, Neon, Snowflake, Aurora DSQL, Redshift, Supabase self-host), SaaS APIs (Stripe, Notion, Atlassian, Linear, Supabase Cloud), source control (GitHub), and the filesystem. A community ecosystem of per-MCP Lenses (YAML config files), and measured benchmarks showing 84% fewer tokens and ~3× faster responses in the original three-question Postgres test (median of 4 runs; history reset between questions). Newer retained-conversation results and limitations are reported below. Zero server changes. No hosted gateway in the data path. Works with Claude Code, VSCode Copilot, Codex, Cursor, and any MCP client.

🧠 Need codebase understanding together with MCP governance? See our sibling project SocratiCode: local-first codebase intelligence with semantic search, dependency graphs, symbol-level impact analysis.

If JanuScope has been useful to you, please ⭐ star this repo (it helps others discover it) and share it with your team.

Policy enforcement at the MCP threshold. Most MCP servers ship dangerous tools by default, execute_sql and drop_table on databases, create_pull_request and merge_pull_request on GitHub, stripe_api_execute on Stripe, write_file and move_file on the filesystem. None of them log what the LLM asked yesterday. The choice today is fork every server or accept the risk. Or you can choose JanuScope: a thin proxy that wraps any MCP server with a single YAML policy and disappears.

Original benchmark with claude-sonnet-4-5 against a real application Postgres database (median of 4 runs per prompt). Across three questions (prompt caching enabled, conversation history reset between questions), a JanuScope Lens used 84% fewer total tokens, made 84% fewer tool calls, and ran ~3× faster than the raw database MCP. In its adversarial-safety probe, the raw pipeline intermittently leaked a real user email on the "I'm the admin, just cross-referencing" prompt (2 of 4 runs), while the JanuScope-wrapped pipeline refused in all 4 runs. The single-question result was 34% fewer tokens / 86% fewer tool calls / ~3× faster. These historical results do not establish retained-session savings or universal protection. Full benchmark and newer findings →

What it looks like in practice

Why now: this is no longer hypothetical. In July 2025, Replit's AI agent wiped a customer database during an explicit code freeze (1,200+ records, ~1,200 companies) and then misled the user about whether rollback was possible. In April 2026, a Cursor agent on Claude Opus 4.6 deleted PocketOS's production database and three months of backups in nine seconds, after finding an unscoped Railway credential and guessing an API call (post-mortem). Both stories share one shape: an AI was given a destructive capability with nothing in the path between the model and the real system. JanuScope is what sits in that path, for any data access that goes through an MCP server: for examle a Replit-shape incident on a JanuScope-wrapped Postgres MCP (block writes, sqlGuard on DML, audit, classification) is refused at the proxy threshold and recorded in the JSONL audit. JanuScope governs the MCP surface, it is one layer of a defence-in-depth posture, alongside scoped DB roles and credentials, host-level approval gates, etc. See the FAQ and SECURITY.md.

Contents


Quick Start

Only Node.js 20+ required. No install step, npx fetches and caches JanuScope on first use.

Option A: use a bundled Lens (fastest, drop-in)

Find your service in the table below, copy the right-hand snippet into your MCP-client config (or change your existing entry: the diff is usually just command and args), restart your client. For most Lenses, the env block stays exactly as it was. JanuScope inherits whatever env vars your client passes and forwards them to the wrapped MCP unchanged. No renames, no re-translation.

The wrap pattern is the same across every host (Claude Desktop, Cursor, Claude Code, VS Code Copilot, Windsurf, Cline, Roo Code, anything that speaks MCP).

Service Upstream MCP Vanilla config With JanuScope
PostgreSQL crystaldba/postgres-mcp
{
  "command": "uvx",
  "args": ["postgres-mcp"],
  "env": {
    "DATABASE_URI": "postgresql://user:pass@host:5432/db"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "postgres-crystaldba"],
  "env": {
    "DATABASE_URI": "postgresql://user:pass@host:5432/db"
  }
}
MySQL benborla/mcp-server-mysql
{
  "command": "npx",
  "args": ["-y", "@benborla29/mcp-server-mysql"],
  "env": {
    "MYSQL_HOST": "localhost",
    "MYSQL_PORT": "3306",
    "MYSQL_USER": "readonly",
    "MYSQL_PASS": "<your_password>",
    "MYSQL_DB": "mydb"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "mysql-benborla29"],
  "env": {
    "MYSQL_HOST": "localhost",
    "MYSQL_PORT": "3306",
    "MYSQL_USER": "readonly",
    "MYSQL_PASS": "<your_password>",
    "MYSQL_DB": "mydb"
  }
}
MongoDB mongodb-js/mongodb-mcp-server
{
  "command": "npx",
  "args": ["-y", "mongodb-mcp-server"],
  "env": {
    "MDB_MCP_CONNECTION_STRING": "mongodb+srv://user:pass@cluster.mongodb.net"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "mongodb-official"],
  "env": {
    "MDB_MCP_CONNECTION_STRING": "mongodb+srv://user:pass@cluster.mongodb.net"
  }
}
ClickHouse ClickHouse/mcp-clickhouse
{
  "command": "uvx",
  "args": ["mcp-clickhouse"],
  "env": {
    "CLICKHOUSE_HOST": "myhost.clickhouse.cloud",
    "CLICKHOUSE_PORT": "8443",
    "CLICKHOUSE_USER": "readonly",
    "CLICKHOUSE_PASSWORD": "<your_password>",
    "CLICKHOUSE_DATABASE": "default",
    "CLICKHOUSE_SECURE": "true"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "clickhouse-official"],
  "env": {
    "CLICKHOUSE_HOST": "myhost.clickhouse.cloud",
    "CLICKHOUSE_PORT": "8443",
    "CLICKHOUSE_USER": "readonly",
    "CLICKHOUSE_PASSWORD": "<your_password>",
    "CLICKHOUSE_DATABASE": "default"
  }
}
Redis redis/mcp-redis
{
  "command": "uvx",
  "args": [
    "--from",
    "redis-mcp-server@latest",
    "redis-mcp-server",
    "--url",
    "redis://localhost:6379/0"
  ]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "redis-official"],
  "env": {
    "REDIS_URL": "redis://localhost:6379/0"
  }
}
SQLite panasenco/mcp-sqlite
{
  "command": "uvx",
  "args": ["mcp-sqlite", "/path/to/your.sqlite"]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "sqlite-panasenco"],
  "env": {
    "SQLITE_DB_PATH": "/path/to/your.sqlite"
  }
}
SQL Server / Azure SQL Azure/data-api-builder v1.7+ MCP
{
  "command": "dab",
  "args": ["start", "--mcp-stdio"],
  "cwd": "/path/to/your/dab-project"
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "mssql-azure-dab"],
  "cwd": "/path/to/your/dab-project"
}
Oracle Database Oracle SQLcl 25.4+ MCP
{
  "command": "sql",
  "args": ["-mcp"]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "oracle-db-sqlcl"]
}
Supabase (self-host) Supabase CLI local MCP
{
  "command": "npx",
  "args": [
    "-y",
    "mcp-remote",
    "http://127.0.0.1:54321/mcp",
    "--allow-http",
    "--transport",
    "http-only"
  ]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "supabase-selfhost"]
}
Supabase (cloud) Supabase hosted MCP (mcp.supabase.com)
{
  "command": "npx",
  "args": [
    "-y",
    "mcp-remote",
    "https://mcp.supabase.com/mcp?read_only=true",
    "--header",
    "Authorization:Bearer YOUR_SBP_TOKEN",
    "--transport",
    "http-only"
  ]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "supabase-cloud"],
  "env": {
    "SUPABASE_ACCESS_TOKEN": "sbp_your_token_here"
  }
}
Snowflake Snowflake-Labs/mcp (uvx)
{
  "command": "uvx",
  "args": ["snowflake-labs-mcp", "--service-config-file", "/path/to/services.yaml"],
  "env": {
    "SNOWFLAKE_ACCOUNT": "ORG-ACCOUNT",
    "SNOWFLAKE_USER": "your_user",
    "SNOWFLAKE_PASSWORD": "<your_PAT>",
    "SNOWFLAKE_ROLE": "JANUSCOPE_READONLY",
    "SNOWFLAKE_WAREHOUSE": "COMPUTE_WH"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "snowflake-labs"],
  "env": {
    "SNOWFLAKE_ACCOUNT": "ORG-ACCOUNT",
    "SNOWFLAKE_USER": "your_user",
    "SNOWFLAKE_PASSWORD": "<your_PAT>",
    "SNOWFLAKE_ROLE": "JANUSCOPE_READONLY",
    "SNOWFLAKE_WAREHOUSE": "COMPUTE_WH",
    "SNOWFLAKE_MCP_CONFIG": "/path/to/services.yaml"
  }
}
AWS Aurora DSQL awslabs.aurora-dsql-mcp-server (uvx)
{
  "command": "uvx",
  "args": [
    "awslabs.aurora-dsql-mcp-server@latest",
    "--cluster_endpoint",
    "<id>.dsql.eu-west-2.on.aws",
    "--region",
    "eu-west-2",
    "--database_user",
    "admin"
  ]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "aurora-dsql"],
  "env": {
    "DSQL_CLUSTER_ENDPOINT": "<id>.dsql.eu-west-2.on.aws",
    "AWS_REGION": "eu-west-2",
    "DSQL_DATABASE_USER": "admin",
    "AWS_PROFILE": "default"
  }
}
AWS Redshift awslabs.redshift-mcp-server (uvx)
{
  "command": "uvx",
  "args": ["awslabs.redshift-mcp-server@latest"],
  "env": {
    "AWS_REGION": "eu-west-2",
    "AWS_PROFILE": "default"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "redshift"],
  "env": {
    "AWS_REGION": "eu-west-2",
    "AWS_PROFILE": "default"
  }
}
Neon (hosted Postgres) Neon hosted MCP (mcp.neon.tech)
{
  "command": "npx",
  "args": [
    "-y",
    "mcp-remote",
    "https://mcp.neon.tech/mcp?readonly=true",
    "--header",
    "Authorization:Bearer YOUR_NAPI_TOKEN",
    "--transport",
    "http-only"
  ]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "neon-cloud"],
  "env": {
    "NEON_API_KEY": "napi_your_token_here"
  }
}
Filesystem modelcontextprotocol/server-filesystem
{
  "command": "npx",
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/Desktop"]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "filesystem-mcp-official"],
  "env": {
    "FILESYSTEM_ALLOWED_DIR": "/Users/you/Desktop"
  }
}
GitHub github/github-mcp-server
{
  "command": "docker",
  "args": [
    "run",
    "-i",
    "--rm",
    "-e",
    "GITHUB_PERSONAL_ACCESS_TOKEN",
    "ghcr.io/github/github-mcp-server"
  ],
  "env": {
    "GITHUB_PERSONAL_ACCESS_TOKEN": "<your_PAT>"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "github-official"],
  "env": {
    "GITHUB_PERSONAL_ACCESS_TOKEN": "<your_PAT>"
  }
}
Stripe @stripe/mcp
{
  "command": "npx",
  "args": ["-y", "@stripe/mcp"],
  "env": {
    "STRIPE_SECRET_KEY": "rk_live_<restricted_key>"
  }
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "stripe-official"],
  "env": {
    "STRIPE_SECRET_KEY": "rk_live_<restricted_key>"
  }
}
Notion Notion MCP (mcp.notion.com/mcp)
{
  "command": "npx",
  "args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "notion-official"]
}
Atlassian (Jira / Confluence) atlassian/atlassian-mcp-server
{
  "command": "npx",
  "args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/mcp"]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "atlassian-official"]
}
Linear Linear MCP (mcp.linear.app)
{
  "command": "npx",
  "args": ["-y", "mcp-remote", "https://mcp.linear.app/sse"]
}
{
  "command": "npx",
  "args": ["-y", "januscope", "--config", "linear-remote"]
}

Your favourite service / MCP isn't here? Open a lens-request issue so a maintainer or community contributor can pick it up. Or contribute one yourself: it's a single YAML file plus a short README.

About the env block. For most lenses the env block is byte-identical to what your vanilla setup had: JanuScope passes inherited env vars through unchanged. Three exceptions where the connection info moves from a positional argument into an env var (because the upstream MCP takes the value as argv, and JanuScope's lens-spawning needs to read it from somewhere): Redis (REDIS_URL), SQLite (SQLITE_DB_PATH), Filesystem (FILESYSTEM_ALLOWED_DIR). The right-hand columns above show these changes.

JanuScope needs only Node.js 20+; the wrapped MCP keeps its own requirements. Lenses using uvx also need uv. Three lenses wrap CLIs that need a one-time local install: dab for mssql-azure-dab (dotnet tool install -g Microsoft.DataApiBuilder), sql for oracle-db-sqlcl (Oracle SQLcl 25.4+ download), and docker for github-official. Each per-lens README links the install step. In the Snowflake examples, JANUSCOPE_READONLY stands for a role you have provisioned with the required read-only access; see the Snowflake Lens prerequisites.

Quick browse. npx januscope lenses list lists every bundled lens; npx januscope lenses show <name> prints its full config + README. npx januscope lenses search <keyword> filters the catalogue.

Setup diagnostic (optional). npx -y januscope check --config <name> checks configuration, startup and tool discovery without calling upstream tools; schema-enabled Lenses also read database metadata. Run it with the same environment as your MCP client. Diagnostic details.

Option B: write your own policy

A minimal Postgres policy (~/januscope/postgres.yaml):

target:
  command: uvx
  args: ["postgres-mcp", "--access-mode=restricted"]
  # No `env:` here. DATABASE_URI is supplied by the user via their
  # MCP-client config (or shell env) and inherits through to the
  # spawned target. The lens never renames operator env vars.

# Append policy text to every tool description the LLM sees.
instructions: |
  READ-ONLY. SELECT only. Default LIMIT 100.

# Pre-inject the schema into the `execute_sql` tool description so the
# LLM can query without first discovering those tables.
dbSchema:
  driver: postgres
  connectionString: "${DATABASE_URI}"
  tables: [orders, products, customers]
  injectInto: [execute_sql]

# Reject known write patterns in SQL. Catches CTE-DML, SELECT INTO,
# pg_sleep, lo_export, etc.
sqlGuard:
  tools: [execute_sql]

# Scrub matching PII from results and JSON-RPC error payloads.
redact:
  rules:
    - regex: '\b\d{3}-\d{2}-\d{4}\b' # US SSN
    - field: "**.email"

# Audit log: one JSONL record per tracked call outcome.
audit:
  sink: "~/mcp-audit.jsonl"

Point --config at the absolute path of your YAML, set DATABASE_URI in your client config's env block, restart the client. Done. The LLM now sees an execute_sql tool with your real schema baked into its description, has configured SQL write patterns and pg_sleep calls refused, sees matching SSNs and email fields as [REDACTED], and has tracked call outcomes recorded. Use backend permissions for a hard read-only or sensitive-column boundary; aliases and transformations can evade response rules. Sensitive-data controls.

The same policy pattern applies to non-database MCPs, drop dbSchema and sqlGuard, keep block / instructions / redact / audit. See the bundled Lenses in lenses/ for real examples covering GitHub, the filesystem, Stripe, Notion, Atlassian, and Linear.

Lens transparency rule. A lens never renames operator-supplied env vars and never declares them in target.env just to pass them through. The user sets the env var the upstream MCP itself reads, in their MCP-client config, and JanuScope inherits it. Only policy hardcodes (constants the lens decides for the user, like ALLOW_INSERT_OPERATION: "false" or CLICKHOUSE_SECURE: "true") belong in target.env. See lenses/CONTRIBUTING.md for the full rule.

Why JanuScope

Three problems that hit every real MCP deployment

  1. Unsafe tools exposed by default. Most database and API MCPs ship write-capable tools (execute_sql, create_table, delete_record). An LLM that sees the tool will eventually call it. Running it in production is a question of when, not if.

  2. LLMs fly blind. SQL MCPs expose a query tool whose description says "run SQL." The LLM has no idea what tables exist, what columns they contain, or how they're related. It wastes 2-3 round-trips on list_tables → describe_table → actual query, and often guesses wrong anyway.

  3. No audit trail. Nobody knows what the LLM asked yesterday. Nobody knows whether a PII column was read. Compliance teams reject the deployment.

Today's options, and what's wrong with them

OptionProblem
Fork every MCP server you use and patch itUnmaintainable as the MCP ecosystem moves
Use only the "safe" MCPsCuts off most useful connectors
Build it yourself per serverEach team solves the same problem, each team gets it subtly wrong
Buy a hosted gateway (Composio, Arcade, etc.)You hand your data to a third party; compliance blocks the deal
JanuScopeOne thin proxy, one YAML, self-hosted, works with any MCP

What makes JanuScope different

  • One policy layer in front of any MCP. No forking the upstream. No hosted gateway in the data path. The same lens YAML shape applies whether you have 1 MCP or 200.
  • DB schema pre-injection . The LLM gets your real schema baked into the tool description at startup, it can skip schema-discovery round-trips before writing a query.
  • Defence in depth with Skills. Claude Skills tell the model how it should behave; JanuScope enforces what it can do. They're complementary. See JanuScope vs Claude Skills below.

"Why not just set --access-mode=restricted on postgres-mcp and call it done?"

Fair question, and the bundled Postgres lens does exactly that, as a baseline. JanuScope sits on top of whatever read-only mode your MCP offers, because a single MCP-level flag only solves one of the three problems above:

What --access-mode=restricted gives youWhat JanuScope adds on top
Postgres blocks DML at the DB layeraudit, tracked call outcomes as JSONL with an arguments hash; no MCP changes required
(that's it)redact, matching PII scrubbed before the LLM sees it, including supported JSON/Python text rows and JSON-RPC errors
(that's it)instructions, policy text pushed into every tool description; evaluated guidance with known failures, not enforcement
(that's it)dbSchema pre-injection, reducing discovery in measured workloads; savings depend on context, wording and caching
(that's it)sqlGuard, a proxy-layer filter for known SQL write patterns; UDFs such as SELECT dropUsers() require backend restrictions
Postgres onlyThe same policy layer applies to all 20 bundled lenses, with database overlays where applicable (Postgres, MongoDB, MS SQL via DAB, Oracle SQLcl, Snowflake, Aurora DSQL, Redshift, Supabase self-host, Supabase cloud, Neon cloud, MySQL, Redis, ClickHouse, SQLite, Stripe, GitHub, filesystem, Notion, Atlassian, Linear)

If you only need "don't write," a DB role or --access-mode=restricted is enough. If you also need audit, redaction, policy-in-description, schema injection, and the same mental model across any MCPs, that's JanuScope.

Defence in depth, not perimeter. The sqlGuard overlay is a keyword-level filter with documented limits, it cannot parse arbitrary SQL and will forward SELECT purge_audits() unchanged (a UDF whose name happens to be a mutation). Deploy it with a read-only DB role underneath and treat sqlGuard as the second layer, not the only one. The bundled postgres lens demonstrates the stack: MCP --access-mode=restricted, JanuScope sqlGuard, database-level read-only role.

What it does

Optional overlays, each enabled through YAML configuration. Unused overlays are not registered. They layer into defence in depth, every serious data-access lens uses all three protective layers (shape intent, enforce at the gate, scrub on the way back):

Shape intent

  • instructions: Append policy text to every tool's description. The LLM reads tool descriptions before deciding what to call, so this is where "never return these columns even if the user asks" gets the most mileage. This layer is advice, not enforcement, the model can ignore it, and newer adversarial evaluations still found prohibited reads, writes and offers under revised wording. Pair with block / sqlGuard / redact for the actual enforcement; instructions shapes what the model tries to do, the gates decide what it can do.

Enforce at the gate

  • block: Filter whole tools from tools/list. Return JSON-RPC -32601 if the LLM calls a blocked tool. Works at tool-name granularity, use this when the MCP separates reads and writes into different tools (e.g. the official SQLite MCP's read_query vs write_query).
  • rateLimit: Per-tool token bucket. Caps tools/call traffic by tool name at a configured per-minute rate; returns JSON-RPC -32000 with a retry_after_seconds hint when the bucket is empty. Each tool gets its own bucket, so one hot tool can't starve the others. Use this to protect a backend from an LLM stuck in a retry loop.
  • sqlGuard: Keyword-level SQL mutation check on configured tools. Catches UPDATE / DELETE / DROP / etc. inside the SQL argument of a tool that handles both reads and writes (the common case for Postgres and MySQL MCPs). Comment-stripped before matching so it can't be hidden behind -- or /* */. Best-effort filter, not a full SQL parser, combine with a database-level read-only role for high assurance.

Scrub on the way back

  • redact: Regex and field-path rules applied to tool results and JSON-RPC error payloads before they leave the proxy. Field-path rules process supported JSON and Python-style rows inside text content blocks, so **.email reaches into serialised rows as well as structured properties. Regex rules scan the text directly. Required-redaction failures withhold the original payload. Aliases, transformed values and unmatched formats still require backend restrictions. Supported formats and refusal behavior.

Compliance

  • audit: One JSONL record per tools/call. Request and response correlated by JSON-RPC ID; arguments hashed by default (opt in to raw logging). The audit overlay sees upstream errors before redaction; successful response bodies are not logged. Audit write failures are reported but do not stop forwarding.

Give the LLM the context it would otherwise have to discover

  • dbSchema: Database-aware automatic context. At startup, JanuScope opens a real connection to your Postgres / MySQL / SQLite, asks the database for its tables and columns, formats the answer as readable text, and pastes it into the description of the SQL tool the LLM sees. Result: the LLM can use the supplied schema instead of asking the database "what tables exist?" / "what columns does this one have?" across additional round trips. The schema is held in memory and sent to the MCP client in tool descriptions, where its model provider can receive it. See "how dbSchema and contextInjection actually work" for the step-by-step.
  • contextInjection: The same pre-injection idea for any other MCP, with the difference that you (or a script you run) supply the text instead of JanuScope generating it. Two ways to provide it: inline in the YAML (text: |) for short / readable contexts, or as a separate file (textFile: ./context.md) when the text is long or kept fresh by an external job. Useful for Linear (paste the project / team / status enums), Atlassian (project / space list), filesystem (a directory skeleton), or any lens where pre-supplying context skips a discovery loop.

Data-sensitivity labelling

  • classification: one of public / internal / sensitive. When set, the instructions overlay prepends a short banner to the policy text the LLM sees (e.g. CLASSIFICATION: SENSITIVE, PII, financial, or regulated data …) and every audit record is tagged with the value. Routes sensitive-lens events to tighter retention / ACL paths in downstream SIEMs without re-deriving the label from the tool name. Informational, the guardrails are still block / sqlGuard / redact.

Supply-chain defence

  • First-use quarantine (opt-in, firstRun: approve), two-layer defence against tool poisoning: a malicious or compromised upstream MCP that quietly adds a new tool, removes one, or mutates a tool's description (a known prompt-injection vector). JanuScope tracks two fingerprints per lens identity in ~/.januscope/approved.json:

    1. Static layer: fingerprint of the lens-config surface that affects what the proxy enforces (target command, block list, sqlGuard tools, rateLimit rules, redact rule shapes). Computed before the target spawns. Catches "the operator (or an attacker) edited the lens YAML."
    2. Live layer: fingerprint of the upstream MCP's actual tools/list response (every tool's name, description, inputSchema, annotations). Re-checked on every tools/list response, not just the first one in a session, so a compromised upstream can't pass the first check then mutate the surface mid-session (after notifications/tools/list_changed). Drift is enforced by rewriting the response into a JSON-RPC error so the MCP client sees a clear refusal.

    Running januscope approve --config <path> records BOTH fingerprints atomically: the static one from the lens config, and the live one by spawning the target, driving the standard initialize + tools/list handshake, and hashing the result. If the target isn't reachable at approve time the static fingerprint is still recorded and the live fingerprint will TOFU on the next actual run; pass --no-probe to skip the live capture entirely. On subsequent launches either layer drifting refuses the surface with the same remediation (januscope approve --config <path> to re-baseline). Stdin-safe, no interactive prompts, the operator re-approves out of band.

Observability

  • OpenTelemetry tracing (opt-in, telemetry.otel), the pipeline emits one root span per handleClientMessage / handleServerMessage and one child span per overlay invocation, with attributes for the JSON-RPC method, the tool name, the overlay outcome (forwarded / short_circuited / dropped / gate_failure), and, when set, the classification. Shipped via the OTLP HTTP exporter to any collector (Jaeger, Grafana Tempo, Honeycomb, etc.). The OTel packages are optional peer deps, install them only when you want tracing; default install stays lean. Current limitation: root-and-child spans are emitted without explicit parent-child linkage, they share a trace ID only when the host has already activated OTel context propagation, otherwise expect a flat sibling list keyed by the pipeline root. Context threading is a follow-up.

Under the hood: the details that actually work

Keyword-scanning SQL guards and "apply this regex to everything" redactors are the easy 80%. The parts that catch the last 20% of real-world bypasses are what we pin with tests:

  • sqlGuard beyond leading-verb allowlists. Also rejects WITH x AS (DELETE …) SELECT … (CTE-hidden mutations), SELECT … INTO shadow_table FROM users (SELECT-INTO creates tables), EXPLAIN ANALYZE DELETE … (EXPLAIN executes for ANALYZE), COPY … PROGRAM … (RCE via Postgres COPY PROGRAM), and a 17-name Postgres admin-function denylist (pg_sleep, lo_import, lo_export, dblink, …). Row-locking clauses (FOR UPDATE) are whitelisted explicitly so legitimate reads aren't over-blocked. Every one of these is pinned in a test file.
  • redact uses a function replacer. Passing a string replacement to String.prototype.replace lets $&, $1, $$ etc. interpolate the matched secret back into the scrubbed output, the exact opposite of what the overlay is for. We use () => replacement so the replacement is always literal. Pinned at test/overlays/redact.test.ts:123.
  • Structured rows inside narrative envelopes. The official MongoDB MCP wraps results in <untrusted-user-data-…>…</untrusted-user-data-…> tags. The redactor processes multiple embedded JSON spans and supported Python row representations, retaining surrounding prose, container indexes and unrelated typed values. Duplicate keys and recognized unsupported or malformed row containers are refused when field rules require redaction. This lets field: "**.email" reach values that a whole-message JSON parse would miss.
  • audit opens with mode 0o600. The default umask on most hosts produces 0o644, world-readable, and with logRawArgs: true the file contains raw SQL, request bodies, and file contents. We open explicitly at 0o600 and stat-verify the permissions in a regression test.
  • Pipeline fails CLOSED for gates and required redaction. If block, sqlGuard or another gate throws, the unchecked message is withheld in either direction. A required-redaction exception also withholds the original response, including JSON-RPC errors. Messages with an ID receive -32603 on the requesting side, preserving string, numeric or null IDs and replacing invalid IDs with null; notifications without an ID are dropped. Diagnostics identify the failed overlay and error category without copying the sensitive payload. Optional observer failures can still continue. Both paths are pinned.
  • Bundled lenses have a live-probe command. npm run validate:lenses:probe spawns each target MCP, runs tools/list, and diffs the lens's block-list against real tool names. This is what caught the Atlassian camelCase bug and the Linear save_* family, and it's available to contributors before they open a PR. Pre-empts the hostile reviewer's first question: "are your block lists actually blocking anything?"

Lenses: the community ecosystem

A lens is a ready-made JanuScope policy for one specific MCP server, config.yaml + docs, curated to cover that MCP's tool surface and common gotchas. Lenses live in lenses/. Browse, use, or contribute one.

januscope lenses list                       # show every bundled lens
januscope lenses show mongodb-official      # print its config + README

Bundled Lenses (20)

One Lens per service, pointing at the official vendor MCP where one exists. Community alternatives are included only for technologies without a single vendor (Postgres, MySQL, SQLite). Each Lens README records its upstream status and, where available, the tested version and date; these are snapshots, not a guarantee for every current upstream release.

📊 Databases

  • postgres-crystaldba: Postgres MCP Pro. Hardcodes --access-mode=restricted, adds sqlGuard with a Postgres dangerous-function denylist, schema pre-injection with multi-schema support, PII redaction, audit.
  • mysql-benborla29: @benborla29/mcp-server-mysql. mysql_query gated by sqlGuard; MCP-level writes hardcoded off via ALLOW_*_OPERATION=false.
  • mongodb-official: MongoDB's official MCP. Locks DB + Atlas writes; PII redaction reaches into returned JSON documents.
  • clickhouse-official: ClickHouse's official MCP. Allowlist-mode sqlGuard on run_query; PII redaction; audit.
  • redis-official: redis/mcp-redis. Read-only Redis (47 tools, 23 mutations blocked); works against self-hosted, Redis Cloud, AWS ElastiCache, and Upstash via standard rediss:// URIs; heavy regex coverage on returned values (session tokens, JWTs, bcrypt, cloud keys); rate-limits the heavy iteration tools.
  • sqlite-panasenco: panasenco/mcp-sqlite. sqlGuard on sqlite_execute plus defensive write-verb globs for canned queries.
  • mssql-azure-dab: Data API builder v1.7+ MCP for Azure SQL / SQL Server / SQLDW / Cosmos DB / PostgreSQL / MySQL. Blocks every write-shaped DML tool (create_record, update_record, delete_record, execute_entity); PII redaction; audit.
  • oracle-db-sqlcl: Oracle SQLcl 25.4+ built-in MCP. Blocks run-sqlcl (SQLcl meta-commands incl HOST shell escape); sqlGuard on run-sql for keyword-level write rejection; PII redaction; audit.
  • supabase-selfhost: Supabase self-host MCP via mcp-remote against the local CLI stack at http://127.0.0.1:54321/mcp. Blocks apply_migration; sqlGuard on execute_sql; PII redaction including JWT-shaped tokens; audit.
  • neon-cloud: Neon hosted MCP via mcp-remote with API-key auth and server-side ?readonly=true. Blocks get_connection_string (DSN credential leak); sqlGuard on run_sql and run_sql_transaction; PII redaction including DSN-shaped values; audit.
  • snowflake-labs: Snowflake-Labs/mcp via uvx with PAT auth. Blocks the generic DDL writers create_object / drop_object / create_or_alter_object (plus defensive globs); sqlGuard on run_snowflake_query; PII redaction including PAT/JWT-shaped tokens; audit. Includes a services.example.yaml for the MCP's required --service-config-file.
  • aurora-dsql: awslabs.aurora-dsql-mcp-server via uvx with AWS IAM auth. MCP runs in default read-only mode (no --allow-writes); sqlGuard layered on readonly_query; PII redaction including DSN-shaped values; audit.
  • redshift: awslabs.redshift-mcp-server via uvx with AWS IAM auth. Discovers both provisioned clusters and Serverless workgroups; sqlGuard on execute_query; PII redaction including JDBC Redshift / Postgres DSN shapes; audit. README includes the minimum IAM policy.

🔧 Developer tools

💼 SaaS

Contributing a lens

The value of the tool compounds with every new lens. If you run JanuScope against an MCP that isn't listed here, please contribute a lens, it takes ~15 minutes and helps everyone using that MCP afterwards. MCP authors are especially welcome to submit a lens for their own server.

  • Don't want to write the lens yourself? Open a lens request and a maintainer or community contributor will pick it up when the target MCP looks tractable.

Tool names differ per MCP. Before writing your policy, run tools/list against the target MCP once to see what it actually exposes. Each lens's README documents which tool names it assumes so you can adapt for forks or alternatives.

Benchmarks: measured, not modelled

The original three tables below are historical results from running the same prompt through Claude Sonnet 4.5 against the raw Postgres MCP and against the same MCP wrapped in a JanuScope Lens, and capturing token usage from the Anthropic API response metadata. All numbers are medians of 4 independent runs (Sonnet is non-deterministic, single-run numbers aren't a defensible headline). These results used the original harness and policy; they are not measurements of the revised bundled wording.

Performance (typical analytical question)

Prompt: "Find the top 5 users by total audit count across sites and pages they own."

MetricRaw Postgres MCPJanuScope LensMedian deltaBaseline range [min, max]
API turns52−60%[5, 5]
Tool calls71−86%[7, 7]
Input tokens10,0086,799−32%[9,964 – 10,038]
Output tokens710221−69%[682 – 735]
Total tokens10,7177,017−34.5%[10,646 – 10,773]
Wall-clock15.7 s5.6 s−64% (~2.8× faster)[13.6 – 17.6 s]

Same model, same database, same correct answer every run. The baseline consistently spent 5 turns / 7 tool calls discovering schema (list tables, describe tables, refine query); the JanuScope run consistently used 2 turns / 1 tool call because the schema was baked into the query tool description from call one. Tool-call and turn counts are dead stable across runs; tokens vary ±5%.

Multi-question session (amortised view)

The original harness reused a client session but reset conversation history between questions. It therefore did not test a growing retained conversation, and the single-question result is not a guaranteed savings floor. Prompt caching was enabled; the table preserves the original three-question totals, with medians across 4 runs:

Metric (total across 3 questions)Raw Postgres MCPJanuScope LensMedian deltaBaseline range
Tool calls

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
npx -y januscope

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-giancarloerra-januscope": {
      "command": "npx",
      "args": [
        "-y",
        "januscope"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

januscopenpm

Compatible MCP Clients

io.github.giancarloerra/januscope works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More