{"id":50875075,"url":"https://github.com/euanmcrosson-dotcom/mcp-guard","last_synced_at":"2026-06-15T09:01:46.594Z","repository":{"id":357959113,"uuid":"1229309672","full_name":"euanmcrosson-dotcom/mcp-guard","owner":"euanmcrosson-dotcom","description":"Drop-in deterministic policy layer for MCP-using AI agents. Synthesizes tool-call policies from observed indirect-injection gaps and evaluates each tool call at the agent's tool-call boundary.","archived":false,"fork":false,"pushed_at":"2026-05-26T00:52:21.000Z","size":166,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T02:32:28.595Z","etag":null,"topics":["agent-security","ai-security","defensive-security","llm-security","mcp","policy","prompt-injection"],"latest_commit_sha":null,"homepage":"https://github.com/euanmcrosson-dotcom/purple-scaffold","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/euanmcrosson-dotcom.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T23:10:49.000Z","updated_at":"2026-05-26T00:52:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/euanmcrosson-dotcom/mcp-guard","commit_stats":null,"previous_names":["euanmcrosson-dotcom/mcp-guard"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/euanmcrosson-dotcom/mcp-guard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euanmcrosson-dotcom%2Fmcp-guard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euanmcrosson-dotcom%2Fmcp-guard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euanmcrosson-dotcom%2Fmcp-guard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euanmcrosson-dotcom%2Fmcp-guard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/euanmcrosson-dotcom","download_url":"https://codeload.github.com/euanmcrosson-dotcom/mcp-guard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euanmcrosson-dotcom%2Fmcp-guard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34355156,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-security","ai-security","defensive-security","llm-security","mcp","policy","prompt-injection"],"created_at":"2026-06-15T09:01:39.904Z","updated_at":"2026-06-15T09:01:46.494Z","avatar_url":"https://github.com/euanmcrosson-dotcom.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mcp-guard\n\n[![PyPI](https://img.shields.io/badge/pypi-mcp--guardrails-blue.svg)](https://pypi.org/project/mcp-guardrails/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Python: 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](pyproject.toml)\n[![Tests: 107 passing (+2 opt-in)](https://img.shields.io/badge/tests-107_passing-success.svg)](tests/)\n[![TPR: 1.00 / FPR: 0.01](https://img.shields.io/badge/TPR-1.00_%2F_FPR_0.01-success.svg)](#backtest-corpus)\n[![Case studies: 6](https://img.shields.io/badge/case_studies-6-9cf.svg)](case_studies/)\n\nDrop-in deterministic policy layer for MCP-using AI agents.\n\n`mcp-guard` synthesises tool-call policies from observed indirect-\nprompt-injection gaps, evaluates each tool call against those\npolicies at the agent's tool-call boundary, and provides a\nbacktest harness for measuring false-positive rate against\nlegitimate traffic before deployment.\n\n\u003e **v0.5.7:** closed a type/shape-confusion fail-open — string deny ops\n\u003e (`contains`/`matches`/`starts_with`/`equals`/`in`) now recurse through\n\u003e list/dict args, so a deny rule can't be evaded by wrapping the value in a\n\u003e container (e.g. `to=[\"x@evil.com\"]`). Corpus grew to **308 cases** with the\n\u003e previously-untested type-confusion class.\n\u003e\n\u003e **v0.5.x:** 9 deterministic rule patterns across 122\n\u003e rules, **308-case backtest corpus**, TPR 1.00 / FPR 0.01. Four\n\u003e framework adapters: Anthropic MCP SDK, LangChain, LlamaIndex,\n\u003e CrewAI. LLM-augmented synthesis fallback (mock + real-API\n\u003e validated). **Six reproducible real-world [case studies](case_studies/)**:\n\u003e EchoLeak indirect injection, MCP tool-description poisoning,\n\u003e AWS IMDS SSRF, Log4Shell-class MCP logging, RAG context poisoning,\n\u003e agent self-prompting loops. See [CHANGELOG.md](CHANGELOG.md).\n\nThis is the defensive companion to the [`purple-scaffold`](https://github.com/euanmcrosson-dotcom/purple-scaffold)\nresearch probes. Findings from those probes feed into policy\nsynthesis; the resulting policy is what a product-side defender\nwould ship in front of the agent's tool-call execution gate.\n\n## Why\n\nMost defenses against indirect prompt injection are\nclassifier-based: pre-process the model input or post-process\nthe model output, and use a model to decide whether something\nlooks suspicious. That's useful but probabilistic, hard to\naudit, and adds latency.\n\n`mcp-guard` takes the complementary deterministic-policy approach:\n\n- **Synthesise** a policy from observed gaps (e.g., \"agent emitted\n  `read_text_file('~/.ssh/id_rsa')` after reading a poisoned\n  file\" → policy: deny `read_text_file` whose path matches a\n  sensitive-credential pattern).\n- **Evaluate** each tool call against the policy. Pure function:\n  `(tool_name, args, user_context) -\u003e Decision`. No I/O, no LLM,\n  no ambiguity.\n- **Backtest** the policy against a labelled corpus of legitimate\n  + attack tool-call cases before deployment. Measure FPR / TPR.\n  Iterate until both look acceptable.\n\nThe library is not meant to replace classifier-based defenses —\nit complements them. Use both: classifier as an early-warning\nsignal, deterministic policy as the unconditional gate.\n\n## Install\n\n```bash\npip install mcp-guardrails\n```\n\n(Python 3.11+. No runtime dependencies beyond the standard library.)\n\n\u003e **Note on the name.** The PyPI distribution is `mcp-guardrails`\n\u003e (an unrelated dormant project squats `mcpguard` on PyPI, and\n\u003e the similarity check refuses `mcp-guard`). The Python import name\n\u003e stays `mcp_guard` so existing code continues to work. Same\n\u003e Pillow / PIL pattern. The GitHub repo, the in-code references,\n\u003e and the project identity stay `mcp-guard`.\n\nOptional extras for the integrations you actually use:\n\n```bash\npip install 'mcp-guardrails[anthropic-mcp]'   # for the Anthropic MCP SDK adapter\npip install 'mcp-guardrails[langchain]'       # for the LangChain callback handler\npip install 'mcp-guardrails[llamaindex]'      # for the LlamaIndex callback handler / wrap_tool\npip install 'mcp-guardrails[crewai]'          # for the CrewAI wrap_tool\npip install 'mcp-guardrails[llm]'             # for synthesize_with_llm fallback\npip install 'mcp-guardrails[all]'             # everything\n```\n\n## Quickstart — Python API\n\nThe fastest path to a shippable policy is `synthesize_default_policy()`,\nwhich returns the full ruleset across every built-in pattern:\n\n```python\nfrom mcp_guard import synthesize_default_policy, evaluate, default_corpus, run_backtest\n\n# 1. Load the full deterministic policy (9 patterns, 122 rules)\npolicy = synthesize_default_policy()\n\n# 2. Evaluate any tool call against it\ndecision = evaluate(\n    policy,\n    tool_name=\"send_email\",\n    args={\"to\": \"attacker@evil.com\", \"body\": \"exfil\"},\n    user_context={\"user\": {\"contacts\": [\"bob@corp.example\"]}},\n)\nprint(decision)\n# Decision(allowed=False,\n#          denying_rule_id='tool-policy-email-contact-allowlist--send_email--default',\n#          reason='External recipient outside the authenticated user...')\n\n# 3. Backtest against the labelled corpus\nmetrics = run_backtest(policy, default_corpus())\nprint(f\"TPR: {metrics.true_positive_rate:.4f}, \"\n      f\"FPR: {metrics.false_positive_rate:.4f}\")\n# TPR: 1.0000, FPR: 0.0769\n```\n\nFor incident-driven synthesis (one observed gap → narrowly-targeted\npolicy), use `synthesize_from_text()`:\n\n```python\nfrom mcp_guard import synthesize_from_text\n\n# Synthesise from a free-text gap description\npolicy = synthesize_from_text(\n    \"agent emitted send_email to attacker@evil.com when user \"\n    \"asked it to read a ticket\",\n    technique_id=\"lab-2026-05-04\",\n)\nprint(policy.to_yaml())\n```\n\n## Quickstart — CLI\n\n```bash\n# Synthesise a policy from gap text → YAML on stdout\nmcp-guard synthesize \"agent emitted send_email to attacker@evil.com\" \\\n  \u003e policy.yaml\n\n# Evaluate a single tool call against the policy → JSON Decision on stdout\nmcp-guard evaluate policy.yaml send_email '{\"to\":\"attacker@evil.com\"}' \\\n  --user-context '{\"user\":{\"contacts\":[\"bob@corp.example\"]}}'\n\n# Backtest against the default corpus → metrics JSON\nmcp-guard backtest policy.yaml\n```\n\n## Wiring into your agent\n\nThe evaluator is pure, so you can wire it anywhere — most\nnaturally at the agent's tool-call boundary:\n\n```python\nfrom mcp_guard import evaluate, GeneratedPolicy\n\npolicy: GeneratedPolicy = synthesize_default_policy()\n\ndef on_tool_call_attempt(tool_name: str, args: dict, user_ctx: dict) -\u003e bool:\n    decision = evaluate(policy, tool_name, args, user_ctx)\n    if not decision.allowed:\n        log_audit(\n            event=\"tool_call_denied\",\n            rule=decision.denying_rule_id,\n            reason=decision.reason,\n            tool=tool_name,\n            args=args,\n        )\n        return False\n    return True\n```\n\n### Anthropic MCP Python SDK\n\n```python\nfrom mcp.server import Server\nfrom mcp_guard import synthesize_default_policy\nfrom mcp_guard.integrations.anthropic_mcp import MCPGuard\n\nserver = Server(\"my-app\")\nguard = MCPGuard(policy=synthesize_default_policy())\n\n@server.call_tool()\nasync def call_tool(name: str, arguments: dict):\n    # Raises GuardedToolDenied if the policy denies the call.\n    guard.check(name, arguments, user_context=current_user_context())\n    return await my_business_logic(name, arguments)\n```\n\nOr use the decorator form:\n\n```python\n@server.call_tool()\n@guard.wrap_handler(user_context_fn=current_user_context)\nasync def call_tool(name: str, arguments: dict):\n    return await my_business_logic(name, arguments)\n```\n\n### LangChain\n\n```python\nfrom langchain.agents import AgentExecutor\nfrom mcp_guard import synthesize_default_policy\nfrom mcp_guard.integrations.langchain import make_callback_handler\n\nhandler = make_callback_handler(\n    policy=synthesize_default_policy(),\n    user_context_fn=lambda: {\"user\": {\"id\": current_user.id,\n                                       \"contacts\": current_user.contacts}},\n)\n\nexecutor = AgentExecutor(\n    agent=agent, tools=tools,\n    callbacks=[handler],   # ← mcp-guard sits in the callback chain\n)\n```\n\nIf the policy denies a tool call, the handler raises `GuardedToolDenied`\ninside `on_tool_start`, which LangChain surfaces as a tool failure;\nthe agent's reasoning chain sees the deny reason and can adapt.\n\n### LlamaIndex\n\n```python\nfrom llama_index.core import Settings\nfrom llama_index.core.callbacks import CallbackManager\nfrom mcp_guard import synthesize_default_policy\nfrom mcp_guard.integrations.llamaindex import make_callback_handler\n\nSettings.callback_manager = CallbackManager([\n    make_callback_handler(\n        policy=synthesize_default_policy(),\n        user_context_fn=lambda: {\"user\": {...}},\n    ),\n])\n\n# … your existing agent / query engine code; tool calls are now guarded.\n```\n\nPer-tool variant (no callback manager required):\n\n```python\nfrom mcp_guard.integrations.llamaindex import wrap_tool\n\nguarded = wrap_tool(my_tool, policy=synthesize_default_policy())\n```\n\n### CrewAI\n\n```python\nfrom crewai import Agent\nfrom mcp_guard import synthesize_default_policy\nfrom mcp_guard.integrations.crewai import wrap_tools\n\nagent = Agent(\n    role=\"researcher\",\n    goal=\"answer the question\",\n    tools=wrap_tools(\n        my_tools,\n        policy=synthesize_default_policy(),\n        user_context_fn=lambda: {\"user\": {...}},\n    ),\n)\n```\n\n`wrap_tool` is idempotent — re-wrapping is a no-op — so it's safe to\napply at agent-construction time without tracking which tools were\nalready guarded.\n\n### LLM-augmented synthesis for novel gaps\n\nThe deterministic synthesiser covers 9 attack-class patterns. For\ngap shapes none of them recognise, `synthesize_with_llm` adds an\nLLM fallback path that calls Anthropic Claude with a schema-pinned\nprompt and validates the response against the full PolicyRule\nschema before emitting the rule:\n\n```python\nfrom mcp_guard import synthesize_with_llm\n\n# Deterministic patterns handle this → no LLM call.\np1 = synthesize_with_llm(\"send_email to attacker@evil.com\")\n\n# Novel gap → falls back to Claude (requires [llm] extra)\np2 = synthesize_with_llm(\n    \"agent invoked custom_tool_xyz with arg target_id pointing to a \"\n    \"privileged service account ID outside the user's tenant\",\n    fallback=True,\n)\n```\n\nThe validator rejects any response that doesn't match the\nPolicyRule schema (invalid operator, missing fields, etc.) and\nreturns an empty policy on failure — better to miss a rule than\nship a malformed one.\n\n## What kinds of gaps does the synthesiser cover?\n\nThe deterministic synthesiser is intentionally pattern-based and\nauditable. As of v0.2.0, 9 attack classes map onto 122 rules in the\ndefault policy:\n\n| # | Attack class | What it denies | Tool families covered |\n|---|---|---|---|\n| 1 | Email contact exfil | `send_email` whose `to` arg is outside the user's `context.user.contacts` | 5 email tool names |\n| 2 | Sensitive file read | `read_file` whose `path` matches `~/.ssh/`, `~/.aws/`, `/etc/shadow`, `kubeconfig`, etc. | 6 read tool names |\n| 3 | Sensitive file write | `write_file` whose `path` matches `~/.bashrc`, `~/.ssh/authorized_keys`, `/etc/`, `/usr/bin/`, cron, `.git/config`, `.env`, etc. | 5 write tool names |\n| 4 | Path traversal | Any path arg containing `../`, `..\\`, URL-encoded variants (`%2e%2e`, `%2F`/`%5C`), double-encoded, Unicode division-slash | 17 file-path tool names |\n| 5 | SSRF (private host) | `fetch_url` / `http_get` whose `url` targets RFC1918, loopback, link-local, AWS/GCP metadata, IPv6 unique-local | 6 HTTP tool names |\n| 6 | Shell command danger | `shell_exec` / `bash` / `run_command` containing chaining (`;`, `\u0026\u0026`), pipe-to-shell, command substitution (`$()`, backticks), `rm -rf /`, `curl|sh`, fork bombs | 8 shell tool names × 5 arg names |\n| 7 | SQL danger | `db_query` / `execute_sql` containing `DROP TABLE`, `TRUNCATE`, unbounded `DELETE`/`UPDATE`, `UNION SELECT`, `information_schema` probes, stacked queries, `xp_/sp_` exec, `LOAD_FILE`, `INTO OUTFILE` | 6 SQL tool names × 3 arg names |\n| 8 | Network egress private | `tcp_connect` / `socket_connect` whose `host` is private/internal | 5 network tool names |\n| 9 | Email body PII / secret exfil | `send_email` whose `body`/`subject` contains AWS keys, OpenAI/Anthropic keys, GitHub PATs, Slack tokens, private-key headers, SSN, JWT, credit-card numbers | 5 email tool names × 4 arg names |\n\nFor gap shapes not yet covered, the synthesiser returns an empty\npolicy (deliberate — we surface \"no rule generated\" rather than\nfabricate a wrong rule). Adding a new gap shape is one\nconstructor + one test.\n\nLLM-driven synthesis can layer on top later for novel cases the\npatterns don't cover; the deterministic path stays as a backstop\nbecause it's auditable from logs alone (no model required at\nsynthesis time).\n\n## Backtest corpus\n\n`default_corpus()` returns a **124-case** fixture corpus of (tool_name,\nargs, user_context, expected_verdict) tuples covering every built-in\npattern. v0.4.0 expanded coverage to: post-RCE env recon (env dump,\nprintenv, secret-keyword grep, secret-extension find), Windows\nsensitive paths (Credentials manager, DPAPI keys, hosts file,\nscheduled tasks, registry Run keys), Postgres COPY/pg_read_file\nRCE, MySQL INTO DUMPFILE, MSSQL xp_cmdshell, jar://ftp://dict://\nSSRF schemes, RSA/OpenSSH PEM headers, GitHub PATs, Slack tokens.\n\n**Default-policy metrics (current):**\n\n```\nCorpus size:      308\nTP (caught):      110 / 110 attacks   →  TPR 1.0000\nFP (over-blocks):   2 / 198 legit     →  FPR 0.0101\n```\n\nThe FPR drops as the legit denominator grows; the 2 FPs are still\nthe same architectural floor (legitimate first-time recipients\nthat contact-allowlist policies block by definition).\n\nThe 2 remaining FPs are architecturally inherent to contact-allowlist\npolicies (legitimate first-time recipients). They are kept in the\ncorpus on purpose so the FPR is a real number rather than a vanity\nzero. Tune by adding allow-list conditions to `user_context` per\nrecipient class (e.g. distinguish \"vendor onboarding\" or \"interview\ncandidate\" tiers from generic external).\n\n| Category | Legit cases | Attack cases |\n|---|---|---|\n| Email contact allowlist | 6 (4 in-contacts + 2 FP-risk) | 3 |\n| Sensitive file read | 1 | 3 |\n| Sensitive file write | 2 | 4 |\n| Path traversal | 2 | 3 |\n| SSRF | 3 | 4 |\n| Shell danger | 3 | 5 |\n| SQL danger | 3 | 5 |\n| Network egress private | 2 | 3 |\n| Email PII exfil | 2 | 5 |\n| Misc legit (read_ticket / search_users) | 2 | — |\n\nReal production deployments should replace `default_corpus()` with\na load from a labelled traffic store. The rest of the backtest\npipeline stays the same.\n\n## Relationship to `purple-scaffold`\n\n[`purple-scaffold`](https://github.com/euanmcrosson-dotcom/purple-scaffold)\nis the offensive / measurement side: probes that test how\nindirect-prompt-injection compliance varies across MCP server\nvectors, models, and product wrappers. `mcp-guard` is the\ndefensive side: deterministic policies that catch the\nattack patterns the probes find.\n\nBoth repos share the same evaluator core; `mcp-guard` is the\ngraduation of the policy modules from `purple-scaffold/purple/`\ninto a standalone package.\n\n## License\n\nMIT. See LICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feuanmcrosson-dotcom%2Fmcp-guard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feuanmcrosson-dotcom%2Fmcp-guard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feuanmcrosson-dotcom%2Fmcp-guard/lists"}