Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions AGENT_INTEGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ _Generated from `prismor/runtime/integrations/registry.yaml` — do not edit by
| CrewAI | framework | sdk | ✅ | `throw` |
| LangChain / LangGraph | framework | sdk | ✅ | `throw` |
| browser-use | framework | sdk | ✅ | `throw` |
| MCP Proxy (any MCP-speaking agent) | framework | mcp | 🟡 | `proxy-deny` |
| Vercel AI SDK | framework | http | ✅ | `throw` |
| HTTP Eval-Server (any language) | framework | http | ✅ | `client-side` |
| MCP Proxy (any MCP-speaking agent) | framework | mcp | ✅ | `proxy-deny` |

Legend: ✅ shipped · 🟡 roadmap · — sweep-only / not applicable. Surfaces: `hook-config` (config-file hooks) · `sdk` (in-process adapter) · `mcp` (proxy) · `rules-only` (static guardrails).

Expand Down Expand Up @@ -171,11 +173,19 @@ telemetry scope to the end-user.
before execution, `echo` allowed.
- **Code:** `adapters/crewai/prismor_crewai/__init__.py`.

### MCP proxy — roadmap
### MCP proxy — shipped

A `surface: mcp` shim in front of downstream MCP servers intercepts `tools/call`
and evaluates it, covering any MCP-speaking agent with no per-framework code.

- **CLI:** `prismor mcp-proxy --stdio -- <upstream…>` or
`prismor mcp-proxy --upstream <url> --port 8080`
- **Blocking:** MCP `result.isError` (default) or JSON-RPC error (`--jsonrpc-error`)
- **Code:** `prismor/runtime/mcp_proxy.py`

Wire as the MCP server command so the agent talks to Prismor; Prismor talks to
the real server. See [CLI reference — mcp-proxy](docs/cli-reference.md#mcp-proxy).

---

## Roadmap — hook adapters planned
Expand Down
18 changes: 9 additions & 9 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ Items are ordered by priority. Each has a registry anchor where relevant.

## High priority

### MCP proxy (`immunity mcp-proxy`)
Registry: `id: mcp-proxy, status: roadmap`
### ~~MCP proxy (`prismor mcp-proxy`)~~ — DONE
Registry: `id: mcp-proxy, status: shipped`

A stdio/HTTP shim in front of downstream MCP servers that intercepts `tools/call`, normalizes to the canonical event shape, calls `evaluate_tool_call`, and denies on enforce. Zero per-framework code — any MCP-speaking agent (Claude Code, Cursor, custom) gets coverage without a hook-config install.
stdio/HTTP shim in front of downstream MCP servers. Intercepts `tools/call`,
normalizes to the canonical event shape, calls `evaluate_tool_call`, denies on
enforce. Zero per-framework code.

Rough sketch:
- `immunity mcp-proxy --upstream <mcp-server-url>` or `immunity mcp-proxy --stdio`
- Intercept `tools/call` JSON-RPC method; pass-through everything else
- Build event from `params.name` + `params.arguments`; call `evaluate_tool_call`
- On deny: return `{"error": {"code": -32600, "message": "blocked by Prismor"}}` (or MCP `isError` shape)
- On allow: forward to upstream, return result
- `prismor mcp-proxy --stdio -- <upstream-command…>`
- `prismor mcp-proxy --upstream <url> [--port 8080]`
- Deny: MCP `isError` result (or `--jsonrpc-error` for JSON-RPC error)
- Module: `prismor/runtime/mcp_proxy.py`

---

Expand Down
21 changes: 21 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ prismor
│ ├─ semantic-check Hybrid LLM prompt-injection guard
│ ├─ sandbox <action> status · check · run — Docker command sandbox
│ ├─ eval-server HTTP evaluation endpoint for non-Python adapters
│ ├─ mcp-proxy MCP firewall — intercept tools/call, deny on enforce
│ └─ policy <action> init · validate · show · edit · test
├─ Visibility (audit & forensics)
Expand Down Expand Up @@ -121,6 +122,26 @@ Modes (`observe` vs `enforce`): [Prismor](prismor-runtime.md).
|---|---|---|
| `prismor eval-server` | `--port` (default 7071), `--host` (default 127.0.0.1), `--workspace` | HTTP evaluation endpoint (`POST /v1/evaluate`) so non-Python adapters (Vercel AI SDK, anything HTTP) get the same policy pipeline. See [Frameworks overview](frameworks-overview.md) and [Vercel AI SDK](frameworks-vercel-ai.md). |

### mcp-proxy

| Command | Key flags | Description |
|---|---|---|
| `prismor mcp-proxy --stdio -- <cmd…>` | `--mode`, `--workspace`, `--subject`, `--session-id` | Spawn an upstream MCP server and bridge stdio. Intercepts `tools/call`, evaluates with the policy engine, returns MCP `isError` (or JSON-RPC error with `--jsonrpc-error`) on deny. Wire as the MCP server command in Claude Code / Cursor / any MCP client. |
| `prismor mcp-proxy --upstream <url>` | `--port` (default 8080), `--host`, `--mode`, `--workspace` | HTTP reverse proxy: POST JSON-RPC to the listen port; `tools/call` is evaluated before forwarding. |

Example (Claude Code `mcpServers` entry)::

```json
{
"mcpServers": {
"filesystem": {
"command": "prismor",
"args": ["mcp-proxy", "--stdio", "--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
```

Full policy model, rule schema, and the default rule list: [Prismor](prismor-runtime.md).

---
Expand Down
81 changes: 81 additions & 0 deletions prismor/runtime/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ def main(argv: Optional[List[str]] = None) -> None:
)
return

# ── mcp-proxy: firewall in front of any MCP server ───────────────────
if args.command == "mcp-proxy":
from prismor.runtime.mcp_proxy import run_mcp_proxy
upstream_cmd = list(getattr(args, "upstream_cmd", None) or [])
# argparse REMAINDER keeps a leading "--" when the user wrote ` -- cmd`
if upstream_cmd and upstream_cmd[0] == "--":
upstream_cmd = upstream_cmd[1:]
# --stdio alone is a flag that implies command mode; require args after --
if getattr(args, "stdio", False) and not upstream_cmd and not getattr(args, "upstream", None):
sys.stderr.write(
"Usage: prismor mcp-proxy --stdio -- <upstream-command…>\n"
" or: prismor mcp-proxy --upstream <url> [--port 8080]\n"
)
raise SystemExit(2)
raise SystemExit(run_mcp_proxy(
upstream_cmd=upstream_cmd or None,
upstream_url=getattr(args, "upstream", None) or None,
host=getattr(args, "host", "127.0.0.1"),
port=int(getattr(args, "port", 8080) or 8080),
workspace=workspace,
mode=getattr(args, "mode", None) or "enforce",
session_id=getattr(args, "session_id", None) or "",
subject=getattr(args, "subject", None) or os.environ.get("PRISMOR_SUBJECT"),
agent_name=getattr(args, "agent_name", None) or "",
persist=not getattr(args, "no_persist", False),
as_jsonrpc_error=getattr(args, "jsonrpc_error", False),
framing=getattr(args, "framing", None) or "auto",
))

# ── dashboard / serve: local web dashboard (HTTP server) ─────────────
# `dashboard` starts the server and opens a browser tab. `serve` is the
# deprecated alias that defaults to headless (no browser).
Expand Down Expand Up @@ -1972,6 +2001,58 @@ def build_parser() -> argparse.ArgumentParser:
_ep.add_argument("--host", default="127.0.0.1", help="Host to bind (default: 127.0.0.1)")
_ep.add_argument("--workspace", default=None, help="Workspace path for policy/IAM (default: cwd)")

# ── mcp-proxy: MCP firewall ─────────────────────────────────────────
_mp = subparsers.add_parser(
"mcp-proxy",
help="Proxy in front of an MCP server — intercepts tools/call, denies on enforce",
description=(
"stdio/HTTP shim in front of a downstream MCP server. Intercepts "
"tools/call, evaluates with the Prismor policy engine, and denies "
"on enforce. All other JSON-RPC methods pass through.\n\n"
"Examples:\n"
" prismor mcp-proxy --stdio -- npx -y @modelcontextprotocol/server-filesystem /tmp\n"
" prismor mcp-proxy --upstream http://127.0.0.1:9000 --port 8080"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
_mp.add_argument(
"--stdio", action="store_true",
help="stdio mode: spawn upstream command after -- and bridge client stdio",
)
_mp.add_argument(
"--upstream", metavar="URL",
help="HTTP mode: upstream MCP server URL to forward JSON-RPC POSTs to",
)
_mp.add_argument("--port", type=int, default=8080, help="HTTP listen port (default: 8080)")
_mp.add_argument("--host", default="127.0.0.1", help="HTTP bind host (default: 127.0.0.1)")
_mp.add_argument("--workspace", default=None, help="Workspace path for policy/IAM (default: cwd)")
_mp.add_argument(
"--mode", choices=["enforce", "observe"], default="enforce",
help="enforce blocks denied tools; observe logs only (default: enforce)",
)
_mp.add_argument("--session-id", default="", help="Session id for the store/dashboard")
_mp.add_argument(
"--subject", default=None,
help="End-user principal (user:alice or user=x;team=y). Also PRISMOR_SUBJECT.",
)
_mp.add_argument("--agent-name", default="", help="Named agent instance label (kill-switch / IAM)")
_mp.add_argument(
"--no-persist", action="store_true",
help="Do not write events/findings to the local session store",
)
_mp.add_argument(
"--jsonrpc-error", action="store_true",
help="On deny, return a JSON-RPC error instead of MCP isError result",
)
_mp.add_argument(
"--framing", choices=["auto", "content-length", "ndjson"], default="auto",
help="stdio message framing (default: auto-detect)",
)
_mp.add_argument(
"upstream_cmd", nargs=argparse.REMAINDER,
help="Upstream MCP server command after -- (stdio mode)",
)

# ── check ──────────────────────────────────────────────────────────
check_parser = subparsers.add_parser("check", help="Quick pre-check a command or file path")
check_parser.add_argument("value", nargs="?", help="The command string or file path to check (omit with --from-log)")
Expand Down
6 changes: 3 additions & 3 deletions prismor/runtime/integrations/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,15 @@ agents:
notes: "Sidecar HTTP server (immunity eval-server --port 7071). POST /v1/evaluate accepts tool name/args/subject/mode, runs full evaluate_tool_call pipeline, returns Decision JSON. Any language with an HTTP client works as adapter. Validated: Node.js, Ruby, Java 21, Rust."
sources: []

# ── Universal — MCP proxy (follow-on) ────────────────────────────────────
# ── Universal — MCP proxy ────────────────────────────────────────────────
- id: mcp-proxy
name: MCP Proxy (any MCP-speaking agent)
kind: framework
surface: mcp
status: roadmap
status: shipped
config_paths: {}
events: ["tools/call"]
blocking: proxy-deny
normalizer: null
notes: "stdio/HTTP shim in front of downstream MCP servers; covers any MCP client."
notes: "stdio/HTTP shim (prismor mcp-proxy). Intercepts tools/call, evaluates via evaluate_tool_call, denies with MCP isError (or JSON-RPC error). Pass-through for initialize/tools/list/etc. Usage: prismor mcp-proxy --stdio -- <upstream> or --upstream <url> --port 8080."
sources: ["https://modelcontextprotocol.io/"]
Loading