{"id":49041998,"url":"https://github.com/tadmstr/scoped-mcp","last_synced_at":"2026-06-14T22:00:43.143Z","repository":{"id":351823956,"uuid":"1212640506","full_name":"TadMSTR/scoped-mcp","owner":"TadMSTR","description":"Per-agent scoped MCP tool proxy — credential isolation, resource scoping, and audit logging for AI agent deployments","archived":false,"fork":false,"pushed_at":"2026-06-13T13:46:46.000Z","size":475,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-13T14:22:58.503Z","etag":null,"topics":["ai-agents","audit-logging","claude-code","credential-isolation","fastmcp","mcp","model-context-protocol","multi-agent","python","tool-proxy"],"latest_commit_sha":null,"homepage":"https://tadmstr.github.io/scoped-mcp/","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/TadMSTR.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-16T15:24:41.000Z","updated_at":"2026-06-13T13:46:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/TadMSTR/scoped-mcp","commit_stats":null,"previous_names":["tadmstr/scoped-mcp"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/TadMSTR/scoped-mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TadMSTR%2Fscoped-mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TadMSTR%2Fscoped-mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TadMSTR%2Fscoped-mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TadMSTR%2Fscoped-mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TadMSTR","download_url":"https://codeload.github.com/TadMSTR/scoped-mcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TadMSTR%2Fscoped-mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34339195,"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-14T02:00:07.365Z","response_time":62,"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":["ai-agents","audit-logging","claude-code","credential-isolation","fastmcp","mcp","model-context-protocol","multi-agent","python","tool-proxy"],"created_at":"2026-04-19T15:08:17.723Z","updated_at":"2026-06-14T22:00:43.108Z","avatar_url":"https://github.com/TadMSTR.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# scoped-mcp\n\n[![Built with Claude Code](https://img.shields.io/badge/Built_with-Claude_Code-6B57FF?logo=claude\u0026logoColor=white)](https://claude.ai/code)\n[![CI](https://github.com/TadMSTR/scoped-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/TadMSTR/scoped-mcp/actions/workflows/ci.yml)\n[![PyPI version](https://img.shields.io/pypi/v/scoped-mcp.svg)](https://pypi.org/project/scoped-mcp/)\n[![Python versions](https://img.shields.io/pypi/pyversions/scoped-mcp.svg)](https://pypi.org/project/scoped-mcp/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nPer-agent scoped MCP tool proxy. One server process per agent — loads only the tools that agent is allowed to use, enforces resource boundaries between agents, holds credentials so agents never see them, and logs every tool call to a structured audit trail.\n\n---\n\n## The Problem\n\nMulti-agent setups (Claude Code subagents, parallel workers, role-based agents) share the same MCP servers. Every agent sees every tool. Every agent holds credentials. Agent A can read Agent B's data. Audit logging is fragmented across a dozen server processes.\n\nExisting solutions solve pieces:\n- **Aggregation gateways** — combine servers, no scoping\n- **Access control proxies** — filter tools per agent, no resource scoping\n- **Credential proxies** — isolate credentials, no tool management\n- **Enterprise gateways** — governance and auth, but cloud and team-oriented\n\nNone combine all four: **tool filtering + resource scoping + credential isolation + audit logging**.\n\nscoped-mcp was built using the same multi-agent pattern it's designed to\nsecure — a research agent evaluated the problem space, a dev agent implemented\nthe code, each with scoped access to only the resources it needed. It runs\nin production as part of [homelab-agent](https://github.com/TadMSTR/homelab-agent),\na self-hosted Claude Code platform with purpose-built agents for different\ninfrastructure domains.\n\n---\n\n## How It Works\n\n```\nAgent process (AGENT_ID=research-01, AGENT_TYPE=research)\n    │\n    ▼\n┌─────────────────────────────────────────┐\n│  scoped-mcp (one process per agent)     │\n│                                         │\n│  ① Load manifest for AGENT_TYPE         │\n│  ② Register allowed tool modules        │\n│  ③ Inject credentials into modules      │\n│  ④ Every tool call:                     │\n│     → enforce resource scope            │\n│     → execute tool logic                │\n│     → write audit log entry             │\n└─────────────────────────────────────────┘\n    │           │           │\n    ▼           ▼           ▼\n Backend A   Backend B   Backend C\n (scoped)    (scoped)    (scoped)\n```\n\n```mermaid\nflowchart LR\n    subgraph agent[\"Agent Process\"]\n        A[\"AGENT_ID=research-01\u003cbr/\u003eAGENT_TYPE=research\"]\n    end\n\n    subgraph proxy[\"scoped-mcp (single process)\"]\n        direction TB\n        M[\"Manifest Loader\u003cbr/\u003e\u003ci\u003eresearch-agent.yml\u003c/i\u003e\"]\n        R[\"Module Registry\"]\n        C[\"Credential Injector\"]\n        EX[\"Tool Execution\u003cbr/\u003e(scope → run → audit)\"]\n\n        M --\u003e R\n        R --\u003e C\n        C --\u003e EX\n    end\n\n    subgraph backends[\"Backends (scoped)\"]\n        FS[\"Filesystem\u003cbr/\u003e\u003ccode\u003eagents/research-01/\u003c/code\u003e\"]\n        DB[\"SQLite\u003cbr/\u003e\u003ccode\u003eagent_research-01.db\u003c/code\u003e\"]\n        NT[\"ntfy\u003cbr/\u003e\u003ccode\u003etopic: research-research-01\u003c/code\u003e\"]\n    end\n\n    ALOG[\"Audit Log\u003cbr/\u003e(JSONL)\"]\n\n    A -- \"MCP (stdio)\" --\u003e proxy\n    EX --\u003e FS\n    EX --\u003e DB\n    EX --\u003e NT\n    EX --\u003e ALOG\n```\n\n---\n\n## Quickstart\n\n```bash\npip install scoped-mcp\n\n# Set agent identity\nexport AGENT_ID=\"research-01\"\nexport AGENT_TYPE=\"research\"\n\n# Run with a manifest\nscoped-mcp --manifest manifests/research-agent.yml\n```\n\n**Claude Code `settings.json`:**\n\n```json\n{\n  \"mcpServers\": {\n    \"tools\": {\n      \"command\": \"scoped-mcp\",\n      \"args\": [\"--manifest\", \"manifests/research-agent.yml\"],\n      \"env\": {\n        \"AGENT_ID\": \"research-01\",\n        \"AGENT_TYPE\": \"research\"\n      }\n    }\n  }\n}\n```\n\nSee `examples/claude-code/` for a complete multi-agent setup.\nSee `examples/launcher/` for stdio subprocess launcher templates — required when proxying\nMCP servers that need credentials, since stdio subprocesses do not inherit the parent env.\n\n---\n\n## Core Concepts\n\n**Agent Identity** — `AGENT_ID` (unique instance) and `AGENT_TYPE` (role) set via environment variables at spawn time. The manifest maps agent types to allowed modules.\n\n**Tool Modules** — one Python file per backend domain. Each module declares its tools, required credentials, and scoping strategy. The framework handles registration, credential injection, and audit wrapping.\n\n**Scoping Strategies** — reusable patterns for resource isolation:\n- `PrefixScope` — file paths, object store keys, cache keys scoped to `agents/{agent_id}/`\n- `NamespaceScope` — key-value operations prefixed with agent's namespace\n- Per-agent file — e.g. SQLite gives each agent its own database file at `{db_dir}/agent_{agent_id}.db`\n- Custom — implement `ScopeStrategy` for your backend's isolation model\n\n**Credential Injection** — backend credentials (API keys, DSNs, tokens) loaded once by the proxy process from environment variables or a secrets file. Modules receive credentials through their context — the agent process never sees them.\n\n**Logging** — two structured JSON-L streams:\n\n1. **Audit log** — what agents did. Every tool call, every scope check. Every entry includes a `session.id` UUID assigned at process start, for correlating all calls within one agent session.\n2. **Operational log** — what the server did. Startup, shutdown, config errors.\n\n**Module Startup** — when an agent connects, scoped-mcp starts all proxied/upstream modules concurrently (`asyncio.gather`) rather than one at a time. With ~17 upstream modules this cuts cold-start from ~5.5s to under 1s — roughly the time of the single slowest module — and removes the window where tools are briefly unavailable during per-connection restarts (e.g. under CloudCLI's stream-json driver). Startup is **fail-loud**: if any module raises during `startup()`, the error propagates and every module that already started is shut down cleanly in a `finally` block, so a partial startup never leaks a subprocess handle. (v1.3.2)\n\n---\n\n## Manifest Format\n\n```yaml\n# manifests/research-agent.yml\nagent_type: research\ndescription: \"Read-only research agent\"\n\nmodules:\n  filesystem:\n    mode: read                # read-only: read_file + list_dir only\n    config:\n      base_path: /data/agents # PrefixScope adds /{agent_id}/ automatically\n\n  sqlite:\n    mode: read\n    config:\n      db_dir: /data/sqlite     # each agent gets /data/sqlite/agent_{agent_id}.db\n\n  ntfy:                       # write-only — no mode field needed\n    config:\n      topic: \"research-{agent_id}\"\n      max_priority: high\n\ncredentials:\n  source: env                 # or \"file\" with path: /run/secrets/agent.yml\n  # or: source: vault — see Vault Credentials section\n\n# Optional: pluggable state backend (required for rate limiting and HITL)\nstate_backend:\n  type: in_process            # default — no external deps\n  # type: dragonfly\n  # url: redis://127.0.0.1:6379/0\n\n# Optional: sliding-window rate limits\nrate_limits:\n  global: 60/minute           # all tools combined\n  per_tool:\n    filesystem_write_file: 10/minute\n    \"mcp_proxy.*\": 30/minute  # glob — all matched tools share one counter\n\n# Optional: argument-value filtering\nargument_filters:\n  - name: no-credentials\n    pattern: '(?i)(password|secret|token)\\s*[:=]\\s*\\S+'\n    fields: [path, query, body]\n    action: block             # or: warn\n    decode: [base64, urlsafe_base64, url]\n\n# Optional: human-in-the-loop approval (requires state_backend.type: dragonfly)\nhitl:\n  approval_required: [\"filesystem_delete_*\", \"sqlite_execute\"]\n  shadow: [\"mcp_proxy.*\"]    # log-only, return synthetic empty success\n  timeout_seconds: 300\n  notify:\n    type: ntfy               # or: log (default), webhook, matrix\n    topic: homelab-hitl\n```\n\n### Environment Variable Substitution\n\nManifest fields support `${VAR_NAME}` placeholders, expanded from the process environment before YAML parsing:\n\n```yaml\nstate_backend:\n  type: dragonfly\n  url: \"redis://:${REDIS_PASSWORD}@host:6379/0\"  # always quote substitution sites\n\ncredentials:\n  source: file\n  path: \"${SECRETS_FILE}\"\n```\n\nRules:\n- Only the braced form is expanded (`${VAR}`, not `$VAR`) to prevent accidental substitution.\n- Undefined variables at startup are a hard error — the agent will not start with incomplete config.\n- Expanded values are never written to audit or ops logs.\n- **Always YAML-quote fields receiving substitution** — a secret value containing `:`, `{`, or `}` can corrupt the YAML structure if the field is unquoted.\n\n### Top-Level Fields and Strict Validation\n\nThe top-level manifest model rejects unknown fields (`extra=\"forbid\"`). A misspelled\nor stale key fails the manifest at load time rather than being silently ignored — a\ndeliberate guard against shadowing attacks, where an unrecognized field could mask a\nreal setting. Every field an agent platform attaches to its manifests must therefore\nbe modeled explicitly.\n\nAlongside the operational fields (`modules`, `credentials`, `state_backend`,\n`rate_limits`, `argument_filters`, `response_filters`, `hitl`, `audit`), the model\naccepts three **platform-metadata** fields. scoped-mcp validates and stores them but\ndoes not act on them — they are consumed by the task dispatcher, agent bus, and other\nagents on the platform:\n\n| Field | Type | Purpose |\n|-------|------|---------|\n| `max_auto_risk` | string | Highest risk tier the agent may auto-approve |\n| `interaction_permissions` | `{auto_approved: [...], needs_approval: [...]}` | Cross-agent task auto-approval lists |\n| `workspace_access` | list of entries (below) | Filesystem paths the agent may access |\n\nEach `workspace_access` entry (added v1.3.3):\n\n| Key | Type | Default | Purpose |\n|-----|------|---------|---------|\n| `path` | string | — | Filesystem path the agent may access |\n| `access` | `readonly` \\| `readwrite` | — | Access mode for the path |\n| `git_backed` | bool | `false` | Path is a git repository |\n| `branch_required` | bool | `false` | Edits must be made on a branch, not the default branch |\n\n```yaml\nworkspace_access:\n  - path: /srv/agents/research-01\n    access: readwrite\n    git_backed: true\n    branch_required: true\n  - path: /srv/shared/reference\n    access: readonly\n```\n\n`workspace_access` was previously tolerated only because the model briefly loosened to\n`extra=\"ignore\"`; modeling it as a typed field lets the top-level model keep\n`extra=\"forbid\"` while still validating the block present in every agent manifest.\n\n### Manifest-to-Tools Mapping\n\n```mermaid\nflowchart LR\n    subgraph manifest_r[\"research-agent.yml\"]\n        MR1[\"filesystem: read\"]\n        MR2[\"sqlite: read\"]\n        MR3[\"ntfy: write-only\"]\n    end\n\n    subgraph tools_r[\"Registered Tools (4)\"]\n        TR1[\"filesystem_read_file\"]\n        TR2[\"filesystem_list_dir\"]\n        TR3[\"sqlite_query\"]\n        TR4[\"ntfy_send\"]\n    end\n\n    MR1 --\u003e TR1 \u0026 TR2\n    MR2 --\u003e TR3\n    MR3 --\u003e TR4\n\n    subgraph manifest_b[\"build-agent.yml\"]\n        MB1[\"filesystem: write\"]\n        MB2[\"sqlite: write\"]\n        MB3[\"ntfy: write-only\"]\n        MB4[\"slack_webhook: write-only\"]\n    end\n\n    subgraph tools_b[\"Registered Tools (8)\"]\n        TB1[\"filesystem_read_file\"]\n        TB2[\"filesystem_list_dir\"]\n        TB3[\"filesystem_write_file\"]\n        TB4[\"filesystem_delete_file\"]\n        TB5[\"sqlite_query\"]\n        TB6[\"sqlite_execute\"]\n        TB7[\"ntfy_send\"]\n        TB8[\"slack_send\"]\n    end\n\n    MB1 --\u003e TB1 \u0026 TB2 \u0026 TB3 \u0026 TB4\n    MB2 --\u003e TB5 \u0026 TB6\n    MB3 --\u003e TB7\n    MB4 --\u003e TB8\n```\n\n---\n\n## Built-in Modules\n\n### Storage\n\n| Module | Scope | Read tools | Write tools |\n|--------|-------|-----------|-------------|\n| `filesystem` | `PrefixScope` — `agents/{agent_id}/` | `read_file`, `list_dir` | `write_file`, `delete_file` |\n| `sqlite` | Per-agent DB file — `{db_dir}/agent_{agent_id}.db` | `query`, `list_tables` | `execute`, `create_table` |\n\n### Notifications\n\nNotification modules are **write-only by design** — every agent needs to send alerts, but no agent should see webhook URLs, SMTP passwords, or API tokens.\n\n| Module | Backend | Credential | Scope |\n|--------|---------|------------|-------|\n| `ntfy` | ntfy.sh (self-hosted or cloud) | Server URL + optional token | Topic per agent (`{agent_id}` template) |\n| `smtp` | Any SMTP server | Host, port, user, password | Configured sender + allowed recipients |\n| `matrix` | Matrix homeserver | Access token | Room allowlist |\n| `slack_webhook` | Slack incoming webhook | Webhook URL | One webhook = one channel |\n| `discord_webhook` | Discord webhook | Webhook URL | One webhook = one channel |\n\n### Proxy\n\n| Module | Description | Key config |\n|--------|-------------|------------|\n| `mcp_proxy` | Forward tool calls to an upstream MCP server (HTTP or stdio) | `url` or `command`, optional `tool_denylist`, `headers` |\n\n`mcp_proxy` connects to upstream MCP servers and re-exposes their tools through scoped-mcp.\nTools are prefixed with the module name (e.g. `memsearch-mcp_search_memory`). Use `tool_denylist`\nto hide specific upstream tools from the agent.\n\n**Header injection** — pass custom HTTP headers to upstream streamable-http servers:\n\n```yaml\nmodules:\n  memsearch-mcp:\n    type: mcp_proxy\n    config:\n      url: http://localhost:8493/mcp\n      headers:\n        Authorization: \"Bearer ${MEMSEARCH_API_TOKEN}\"\n```\n\nHeader values support `${VAR}` substitution (same rules as all manifest fields).\nHeaders are only applied to HTTP transports — configuring headers on a stdio\ntransport logs a warning and ignores them. `Authorization` header values are\nautomatically redacted from structured logs.\n\n### Infrastructure\n\n| Module | Scope | Read tools | Write tools |\n|--------|-------|-----------|-------------|\n| `http_proxy` | Service allowlist + SSRF prevention | `get` | `post`, `put`, `delete` |\n| `grafana` | Folder-based (`agent-{agent_id}/`) | `list_dashboards`, `get_dashboard`, `query_datasource`, `list_datasources` | `create_dashboard`, `update_dashboard`, `create_alert_rule`, `delete_dashboard` |\n| `influxdb` | Bucket allowlist + `NamespaceScope` | `query`, `list_measurements`, `get_schema` | `write_points`, `create_bucket`, `delete_points` |\n\n### Credentials\n\nEvery module declares its required and optional environment variables. scoped-mcp\nfails at startup with a clear error listing any missing required keys — it will not\nstart partially configured.\n\n| Module | Required env vars | Optional env vars |\n|--------|------------------|-------------------|\n| `filesystem` | — | — |\n| `sqlite` | — | — |\n| `ntfy` | `NTFY_URL` | `NTFY_TOKEN` |\n| `smtp` | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD` | — |\n| `matrix` | `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` | — |\n| `slack_webhook` | `SLACK_WEBHOOK_URL` | — |\n| `discord_webhook` | `DISCORD_WEBHOOK_URL` | — |\n| `http_proxy` | — (dynamic; see module config) | — |\n| `grafana` | `GRAFANA_URL`, `GRAFANA_SERVICE_ACCOUNT_TOKEN` | — |\n| `influxdb` | `INFLUXDB_URL`, `INFLUXDB_TOKEN` | `INFLUXDB_ORG` (overrides `config.org`) |\n\nCredentials are passed in `settings.json` under `env` (for Claude Code) or exported\nin the shell before running `scoped-mcp`. They are loaded once at startup, injected\ninto module contexts, and never returned in tool responses or logged.\n\nFor HashiCorp Vault — set `credentials.source: vault` in the manifest with an\n`approle` block; credentials are fetched once at startup and the client token is\nrenewed in the background. Requires `pip install scoped-mcp[vault]`. See\n`examples/vault/` for a working manifest, AppRole setup script, and Vault policy.\n\nFor integration with a secrets manager such as Vaultwarden, see\n`examples/vaultwarden/`.\n\n---\n\n## Three-Module Workflow\n\n```\n┌─ ops-agent (AGENT_ID=ops-01) ────────────────────────────────────┐\n│                                                                   │\n│  1. influxdb_query(bucket=\"metrics\",                             │\n│       filters=[{\"field\": \"_measurement\",                         │\n│                 \"op\": \"==\", \"value\": \"docker_cpu\"}])             │\n│     → discovers container X averaging 94% CPU                    │\n│                                                                   │\n│  2. grafana_create_dashboard(                                     │\n│       title=\"Container Health\",                                  │\n│       panels=[{\"title\": \"CPU by Container\", ...}])               │\n│     → dashboard created in folder agent-ops-01/                  │\n│                                                                   │\n│  3. ntfy_send(title=\"High CPU: container X\",                     │\n│       message=\"Averaging 94% over last hour.\")                   │\n│     → operator gets push notification                            │\n│                                                                   │\n└───────────────────────────────────────────────────────────────────┘\n```\n\nThe agent queried metrics it can see, built a dashboard it owns, and alerted through a channel it's allowed to use. At no point did it see API tokens, access another agent's data, or modify operator dashboards.\n\n---\n\n## Write Your Own Module\n\n```python\n# src/scoped_mcp/modules/redis.py\nfrom scoped_mcp.modules._base import ToolModule, tool\nfrom scoped_mcp.scoping import NamespaceScope\n\nclass RedisModule(ToolModule):\n    name = \"redis\"\n    scoping = NamespaceScope()\n    required_credentials = [\"REDIS_URL\"]\n\n    def __init__(self, agent_ctx, credentials, config):\n        super().__init__(agent_ctx, credentials, config)\n        import redis.asyncio as aioredis\n        self._redis = aioredis.from_url(credentials[\"REDIS_URL\"])\n\n    @tool(mode=\"read\")\n    async def get_key(self, key: str) -\u003e str | None:\n        \"\"\"Get a value (scoped to agent namespace).\"\"\"\n        scoped_key = self.scoping.apply(key, self.agent_ctx)\n        return await self._redis.get(scoped_key)\n\n    @tool(mode=\"write\")\n    async def set_key(self, key: str, value: str, ttl: int = 0) -\u003e bool:\n        \"\"\"Set a key-value pair (scoped to agent namespace).\"\"\"\n        scoped_key = self.scoping.apply(key, self.agent_ctx)\n        return await self._redis.set(scoped_key, value, ex=ttl or None)\n```\n\nAdd it to your manifest:\n```yaml\nmodules:\n  redis:\n    mode: read     # only get_key registered\n    config: {}\n```\n\nSee `examples/custom-module/` for a full walkthrough and `docs/module-authoring.md` for the complete contract.\n\n---\n\n## Comparison to Existing Tools\n\n| Capability | scoped-mcp | agent-mcp-gateway | local-mcp-gateway | Kong MCP |\n|---|---|---|---|---|\n| Tool aggregation | yes | yes | yes | yes |\n| Per-agent tool filtering | manifest | rules file | profiles | RBAC |\n| Resource scoping | **yes** | no | no | no |\n| Credential isolation | **yes** | no | no | partial |\n| Unified audit log | **yes** | no | no | yes |\n| Read/write modes | **yes** | per-tool | per-profile | per-role |\n| Self-hosted, single process | yes | yes | yes | no |\n| Built-in modules | 10 | 0 | 0 | 0 |\n\n---\n\n## Security\n\nscoped-mcp's core value is security — tool scoping, credential isolation, and\naudit logging. To back that up:\n\n- **Threat model:** `docs/threat-model.md` documents the attack surface,\n  trust boundaries, and what scoped-mcp does and does not protect against.\n- **Audit history:** `docs/security-audit.md` tracks formal internal audits:\n  v0.1.0 found 18 findings (1 critical, 3 high, 8 medium, 6 low), remediated\n  in v0.2.0; the v0.2.1 follow-up audit returned clean. Post-v1.0 security\n  fixes (OTel exception redaction, audit log stdio isolation, ManifestError\n  secret suppression) are documented in CHANGELOG.md.\n- **Verifiable isolation:** the `examples/claude-code/multi-agent-setup.md`\n  includes a step-by-step verification walkthrough — you can confirm filesystem\n  isolation and credential non-exposure yourself in under five minutes.\n\n### Optional guardrails\n\nSix opt-in middleware layers sit on top of the core tool/scope/credential/audit\nguarantees. All are off by default; enable per-agent in the manifest:\n\n- **OpenTelemetry tracing** (`OTEL_EXPORTER_OTLP_ENDPOINT`, v0.6) — one span per\n  tool call with `scoped_mcp.*` attributes (`agent.id`, `agent.type`, `tool.name`,\n  `call.status`). Auto-enabled when `OTEL_EXPORTER_OTLP_ENDPOINT` is set in the\n  environment. Tool arguments are excluded from spans to prevent credential leakage.\n  Works with SigNoz, Grafana Tempo, Jaeger, and Langfuse OTLP ingest. Requires\n  `pip install scoped-mcp[otel]`.\n\n- **Rate limiting** (`rate_limits:`, v0.7) — sliding-window per-agent and\n  per-tool limits with glob patterns. Backed by `InProcessBackend` (default)\n  or `DragonflyBackend` (`[dragonfly]` extra) for cross-process state.\n- **Vault-backed credentials** (`credentials.source: vault`, v0.8) — fetch\n  credentials from HashiCorp Vault via AppRole; client token auto-renewed in\n  the background. See `examples/vault/`.\n- **mcp_proxy schema validation + argument filtering** (`argument_filters:`,\n  v0.9) — proxied calls are validated against the upstream tool's\n  `inputSchema` before forwarding; pattern-based argument filters can block\n  or alert on values, with optional base64/url decoding. See\n  `docs/threat-model.md` for the documented limits.\n- **Human-in-the-loop approval** (`hitl:`, v1.1) — operator-gated tool\n  calls using a reject-then-wait design. When an agent calls an\n  `approval_required` tool, the middleware rejects immediately with a\n  `HitlRejectedError` containing an approval ID and retry instructions —\n  the MCP connection stays open. The operator runs\n  `scoped-mcp hitl approve \u003cid\u003e`, which writes a one-time pre-approval\n  token to Dragonfly (60 s TTL). The agent retries the tool call; the\n  middleware finds and consumes the token and forwards the call upstream.\n  Shadow-mode tools log a sanitised argument summary and return a\n  synthetic empty-success without forwarding upstream — useful for\n  observing agent behaviour before enabling a tool.\n\n  CLI subcommands:\n  ```\n  scoped-mcp hitl list                      # pending approvals\n  scoped-mcp hitl approve \u003capproval_id\u003e     # write pre-approval token\n  scoped-mcp hitl reject  \u003capproval_id\u003e     # delete pending key\n  ```\n\n  Requires `state_backend.type: dragonfly`. Install with\n  `pip install scoped-mcp[dragonfly]`.\n\n- **Response filtering** (v1.0.2) — opt-in post-execution content scanning.\n  `block`, `warn`, or `redact` modes applied per-field via `ResponseFilterRule`\n  entries in the manifest's `audit:` section. Redaction applies to string leaves\n  in structured responses only — never to serialized dict/list blobs. See\n  `contrib/response_filter.py`.\n\n---\n\n## Non-Goals\n\n- **Not an enterprise gateway** — no OAuth, no multi-tenant SaaS, no Kubernetes. For self-hosters running multi-agent setups.\n- **Not a policy engine** — no prompt injection detection, no tool call classification.\n- **Not a process manager** — one MCP server that an agent connects to. Spawning agents is your orchestrator's job.\n- **Not E2EE** — the Matrix module supports unencrypted rooms only (no libolm dependency).\n\n---\n\n## Installation\n\n```bash\n# Core only (filesystem + sqlite + notifications require no extras)\npip install scoped-mcp\n\n# With HTTP client modules (http_proxy, grafana, influxdb, ntfy, matrix, slack, discord)\npip install \"scoped-mcp[http]\"\n\n# With SMTP support\npip install \"scoped-mcp[smtp]\"\n\n# With SQLite async support\npip install \"scoped-mcp[sqlite]\"\n\n# With OpenTelemetry tracing (auto-enabled when OTEL_EXPORTER_OTLP_ENDPOINT is set)\npip install \"scoped-mcp[otel]\"\n\n# With shared state backend for rate limiting and HITL across processes\npip install \"scoped-mcp[dragonfly]\"\n\n# With HashiCorp Vault credential source\npip install \"scoped-mcp[vault]\"\n\n# HTTP + SMTP + SQLite bundle (does not include otel, dragonfly, or vault)\npip install \"scoped-mcp[all]\"\n```\n\nIf something isn't working, see [Troubleshooting](docs/troubleshooting.md).\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftadmstr%2Fscoped-mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftadmstr%2Fscoped-mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftadmstr%2Fscoped-mcp/lists"}