Skip to content
Cloud-Ops-DevPublic

About

An MCP gateway and capability broker — one small tool surface in front of every downstream MCP server, with deny-by-default policy, credential isolation, and audit.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Janus — MCP Gateway & Capability Broker

Janus

An MCP gateway and capability broker.
One small, stable tool surface in front of every downstream MCP server — just-in-time discovery, deny-by-default policy, credential isolation, and a full audit trail.

Python 3.11+ MCP Tests mypy strict Status


The problem

Every capable agent loads its tools by connecting to MCP servers. As an operator adds servers — issue tracker, notes/memory, project board, databases, billing, deploy tooling — every session pays the full context cost of every tool schema, the model's tool-selection accuracy degrades, and credentials for all of those services are scattered across per-agent configs.

Loading N servers into every session does not scale.

What Janus does

An agent loads one server — Janus — which exposes a small, fixed set of broker tools and keeps the dozens of downstream tools as an implementation detail behind a registry, policy engine, credential broker, sanitizer, and audit log:

capability.search(query)      → a short, ranked list of relevant capabilities (no full schemas)
capability.describe(id)       → the schema + risk tier + policy for one capability
capability.call(id, args)     → a policy-checked, credential-injected, audited invocation
server.list / server.health   → downstream inventory + liveness
policy.explain(id)            → why an action is allowed / denied / needs confirmation
audit.recent(limit)           → the recent invocation log

The model sees ~7 broker tools instead of 100+ downstream tools. Capabilities are discovered just in time (search → describe → call) rather than dumped up front, and every call is brokered: checked against policy, given credentials the model never sees, size-capped and secret-redacted, and logged.

Architecture

Janus architecture: clients → gateway (registry · policy · credentials · sanitizer · audit · drift) → downstream services

Clients (Claude Code, Codex, an SSH/REST consumer) reach Janus over MCP or the REST/CLI fallback. The gateway resolves each call through five cooperating subsystems before it ever touches a downstream server:

Subsystem Responsibility
Registry Source-of-truth catalog of servers + capabilities (transport, trust level, risk ceiling, env scope). Public-repo-safe YAML seed; runtime state in a local SQLite SchemaStore.
Policy engine Deny-by-default decision per call from (profile, risk tier, environment) → allow / confirm / deny, with a human-readable reason.
Credential broker Resolves secret references (e.g. op://…) at call time, injects them into the downstream env/headers, caches them in memory with a TTL, and never logs or returns them.
Sanitizer Caps output size, redacts secrets, and labels untrusted downstream content before it reaches the model.
Audit + drift Append-only JSONL + SQLite invocation log; descriptor/schema drift detection that auto-quarantines a changed capability until it is re-approved.

Key properties

  • Small fixed tool surface → lower context cost and better tool selection.
  • Staged just-in-time discovery — search → describe → call, never a full schema dump.
  • Deny-by-default policy with risk tiers (read_only … local_write … prod_write … destructive / financial) and environment gates (dev / test / prod_safe / prod), scoped per agent profile.
  • Credentials owned by the gateway, resolved from an external secret manager at call time — never exposed to the model or written to logs.
  • Untrusted tool metadata neutralized — human-reviewed summaries; descriptor/schema drift is detected and quarantined.
  • Every invocation audited.
  • Dual interface — MCP for capable hosts, REST/CLI for everything else.
  • Self-sufficient — runs as a systemd --user service that survives a reboot with no shell session present; --check fails loudly on missing configuration.

Project status

Janus is built in deliberate phases — value lands early; risk (write tools, lazy lifecycle) is added only after policy and audit are solid.

Phase Scope Status
1 Static registry · 7 broker tools · policy engine · credential broker · audit · REST/CLI · systemd ✅ Implemented & deployed
2 Downstream discovery crawler · approval workflow · descriptor-drift auto-quarantine + alerts ✅ Implemented
3 Full policy engine · write tools behind confirmation · lethal-trifecta session tracking ⏳ Planned
4 Lazy downstream lifecycle (on-demand start, idle shutdown, circuit breakers) ⏳ Planned
5 Semantic capability search (embeddings over sanitized summaries) ⏳ Planned
6 Dynamic native-tool exposure for hosts that support tools/list_changed ⏳ Optional

The reference deployment currently brokers Beads and Paperclip (read-only). An Open Brain memory downstream is pending multi-header HTTP auth support.

Quick start

# 1. Environment (Python 3.11+)
uv venv && uv pip install -e .

# 2. Configure — copy the env template and fill in per-host tokens + downstream endpoints
cp config/janus.env.template janus.env   # then edit; keep it out of git (gitignored)

# 3. Validate configuration (the systemd ExecStartPre gate — exits non-zero on problems)
python -m janus --check

# 4. Serve
python -m janus --serve       # REST API + authenticated MCP at /mcp/
python -m janus --mcp-http    # standalone authenticated MCP-over-HTTP
python -m janus --stdio       # MCP over stdio (per-session spawn)

Run it as a managed service with the bundled unit:

cp systemd/janus.service ~/.config/systemd/user/
systemctl --user enable --now janus.service
loginctl enable-linger     # survive logout / reboot

CLI

bin/janus — thin client over the REST API for SSH / scripted use. Reads JANUS_URL (default http://127.0.0.1:8088) and a per-host JANUS_TOKEN from the environment:

bin/janus search "open issues assigned to me"
bin/janus describe <capability-id>
bin/janus call <capability-id> '{"arg": "value"}'
bin/janus servers          # inventory + health
bin/janus explain <capability-id>
bin/janus audit --limit 20

Networked MCP clients connect to http://HOST:8088/mcp/ with a bearer token declared in JANUS_TOKENS. Each Mcp-Session-Id receives an isolated broker, audit and trifecta key (<token-label>:mcp:<session-id>), and dynamic tool view. Capability allowlists compare the authenticated token label. The session key stays the audit and trifecta id. Idle session state expires after JANUS_MCP_SESSION_TTL_SECONDS (default: one hour). Missing or unknown tokens are rejected.

bin/janus-admin — host-local administration (talks to the registry SQLite directly, never over the network):

bin/janus-admin discover                       # crawl downstreams, refresh observations
bin/janus-admin list                           # every capability's lifecycle state
bin/janus-admin pending                        # capabilities awaiting first approval
bin/janus-admin approve <id>                   # approve + lock the reviewed baseline
bin/janus-admin diff <id>                      # baseline-vs-observed descriptor delta
bin/janus-admin quarantine-capability <id>
bin/janus-admin quarantine-server <id>

It targets the live registry — the same one the running gateway serves — by sourcing the gateway env (JANUS_GATEWAY_ENV, default ~/.config/systemd/user/janus.env) unless you pin JANUS_CONFIG_DIR / JANUS_DATA_DIR yourself. Every run prints the resolved config_dir= and data_dir= on stderr, and warns when the target is the repo-local copy. Check that line before trusting any output: an earlier version defaulted to <repo>/data, so it reported a clean 27-capability registry while the live one held 134 with a real quarantine — and an approve written there never reached the broker. See docs/operations.md → "Which registry am I editing?".

Configuration

All configuration lives in config/ and is public-repo-safe — it contains no secrets, tokens, op:// references, or internal endpoints. Connection details and credentials are supplied at runtime via the named environment variables the credential broker resolves.

File Purpose
servers.yaml Downstream server registry — transport, trust level, risk ceiling, env scope.
capabilities.yaml Per-capability summaries, tags, and risk tiers (the searchable surface).
profiles.yaml Agent profiles (e.g. default_assistant, infra_operator) → allowed / confirm / denied risk tiers per environment.
janus.env.template Documented template for the per-host env file (downstream URLs + tokens, JANUS_TOKENS, optional alert webhook).

Security model

  • No secret ever reaches the model or the audit log — the credential broker injects them downstream and the sanitizer redacts them on the way back. This is asserted by tests.
  • Deny-by-default — a capability is uncallable until it is approved, and any risk tier above a profile's allowance is denied or gated behind confirmation, with policy.explain giving the reason.
  • Room identities do not self-confirm. A JANUS_TOKENS entry ending in :room_approval cannot authorize a confirm-tier or lethal-trifecta call with confirm=true. The call runs once only after a separate operator bearer approves that exact request. An X-Retinue-Room-Binding MCP header, when present, is pinned to that session and returned only on the operator approval view. The approval routes are not MCP tools. See docs/operations.md.
  • Tool-poisoning resistance — model-visible text comes from human-reviewed summaries, not raw downstream descriptions; a descriptor/schema change auto-quarantines the capability until it is re-reviewed.
  • Public GitHub issue lookup — github_public.issue_get reads one issue (title, state, updated time, URL) from an explicit repository allowlist. It is read-only, limited to the Janus principal retinue, and does not fetch arbitrary URLs or mutate issues. See docs/operations.md.

Development

uv run ruff check .          # lint (includes bandit security rules)
uv run mypy src              # strict type checking
uv run pytest                # 236 tests

The substrate is the official modelcontextprotocol/python-sdk (ClientSessionGroup for downstream connections) plus fastmcp for the broker surface. Janus keeps a thin adapter over both so SDK churn stays localized.

Repository layout

janus/
├── bin/                  janus (REST client) · janus-admin (local admin)
├── config/               servers · capabilities · profiles · env template
├── docs/                 documentation + diagrams
├── src/janus/
│   ├── registry/         registry loader + SQLite SchemaStore
│   ├── downstream/       ClientSessionGroup manager (stdio + HTTP, tolerant connect)
│   ├── policy/           deny-by-default profile policy engine
│   ├── security/         credential broker · secret redactor · output sanitizer
│   ├── audit/            JSONL + SQLite invocation log
│   ├── discovery/        crawler · drift detection · alerts
│   ├── admin/            approval / quarantine CLI service
│   ├── broker.py         the 7 broker tools' logic
│   ├── server_mcp.py     FastMCP surface
│   ├── server_rest.py    FastAPI REST mirror
│   └── gateway.py        composition root
└── tests/                unit + integration (fake downstream MCP server)

License

To be determined. Until a license file is added, all rights are reserved by the authors.

About

An MCP gateway and capability broker — one small tool surface in front of every downstream MCP server, with deny-by-default policy, credential isolation, and audit.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages