MCP Toolshed
by derrickSh43
README.md
# MCP Toolshed
MCP Toolshed is a pluggable, policy-aware MCP server that gives agents one
controlled catalogue of tools while the actual providers remain distributed
across private networks, clusters, cloud accounts, and external vendors.
It is designed to run behind an identity-aware gateway such as Kong. It can
also call an independent Agentic Firewall-compatible inspection service before
and after provider execution. The projects communicate through HTTP contracts;
they do not need to share source code or a database.
> This repository is a reference implementation. The bundled providers,
> identities, endpoints, and Kubernetes resources contain synthetic example
> data and must be replaced before production deployment.
## Why a toolshed
Agents should not receive unrestricted network access, provider credentials, or
every tool that happens to be online. Toolshed introduces a stable control
point between an authorized MCP request and separately owned tool providers.
It provides:
- filtered `tools/list` results for each verified agent client ID;
- `search_tools` and `get_tool_schema` for large catalogues;
- fixed, administrator-owned provider destinations;
- explicit `internal` or `external` network scope independent of protocol;
- pinned input and output JSON Schemas;
- per-agent/per-tool rate limits and per-tool concurrency limits;
- provider-wide bulkheads;
- separate provider credentials that are never taken from tool arguments;
- Agentic Firewall ingress and egress inspection;
- production-deny-by-default external MCP connectivity;
- explicit private-tunnel or controlled-proxy egress with optional mTLS;
- remote redirects and inherited environment proxies disabled;
- result-size limits, credential-field redaction, and untrusted-output marking;
- structured audit events without logging arguments or provider responses; and
- internal HTTP and remote Streamable HTTP MCP provider adapters.
## Architecture
```mermaid
flowchart LR
Agent["Agent runtime\nMCP client"]
Kong["Kong Gateway\nidentity, traffic controls"]
Enforcer["Authorization enforcer\ntrusted action + OPA + approval"]
Toolshed["MCP Toolshed\ndiscovery, schemas, quotas, dispatch"]
Firewall["Agentic Firewall\ningress and egress inspection"]
Registry["Reviewed tool registry"]
Redis["Redis\nshared quotas"]
Egress["Private tunnel or egress gateway\nnetwork destination enforcement"]
subgraph Providers["Separately hosted providers"]
Internal["Internal REST tools"]
Remote["Remote MCP servers"]
SaaS["SaaS and cloud APIs"]
end
Agent -->|"JSON-RPC /mcp"| Kong --> Enforcer --> Toolshed
Registry -. "allowed tools, schemas, fixed endpoints" .-> Toolshed
Redis -. "multi-replica counters" .-> Toolshed
Toolshed -. "inspect arguments and results" .-> Firewall
Toolshed --> Internal
Toolshed --> Egress --> Remote
Egress --> SaaS
```
The MCP request happens in the agent's MCP client. The client sends JSON-RPC to
Kong `/mcp`; it does not embed this server. After gateway authorization,
Toolshed selects a reviewed provider from its registry and performs the final
dispatch.
Agentic Firewall participates twice in each dispatch: before provider execution
and before the result is returned. External providers are blocked by default in
production. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full trust-boundary
and data-flow view.
## Request and response flow
1. The agent obtains an access token and sends an MCP request to Kong.
2. Kong applies route, size, rate, and telemetry controls.
3. The authorization enforcer verifies identity, creates a trusted action,
checks approval when required, and asks OPA.
4. The enforcer removes caller-supplied trusted headers, recreates verified
identity context, and forwards the allowed request to Toolshed.
5. Toolshed exposes only tools assigned to the verified agent client ID.
6. For a tool call, Toolshed validates arguments against the pinned schema and
applies the agent/tool quota and concurrency budgets.
7. Agentic Firewall inspects the arguments. In `enforce` mode, an outage or
non-allow decision blocks execution.
8. Toolshed calls the fixed provider endpoint with only that provider's
credential. Remote MCP calls must also pass the selected `blocked`,
`private`, or `proxy` network mode. The caller cannot supply a destination,
credential, proxy, or route.
9. Toolshed bounds and validates the result, redacts credential-shaped fields,
and asks Agentic Firewall to inspect the response.
10. The result is marked `untrusted_tool_output`, assigned an audit event ID,
and returned through the gateway to the agent.
## Included demonstration tools
The examples prove that providers do not need to live with Toolshed:
| Public tool | Transport | Demonstrates |
| --- | --- | --- |
| `internal.service_status` | Internal HTTP | A private business or operations API |
| `external.public_status` | Remote MCP | A separately hosted external-style MCP server |
Both return synthetic status data. No real service credentials or customer
information are included.
## Real-world workflow
Consider an incident-response agent that needs tools owned by several teams:
1. It searches Toolshed and sees only the incident, cloud-read, ticketing, and
approved remediation tools assigned to its identity.
2. It calls an internal findings provider using a narrowly scoped workload
credential held by that provider connector.
3. It attaches sanitized evidence through an external ticketing provider. The
SaaS token stays in the connector secret and never enters the prompt.
4. It requests a destructive production action. The upstream gateway requires
a human approval bound to the exact actor, tool, environment, and argument
digest before Toolshed receives the call.
5. Toolshed validates the approved arguments, inspects them, applies quotas,
and dispatches to a provider whose cloud role or Kubernetes service account
permits only that narrow operation.
6. The result is validated and inspected before the agent receives it, with an
event ID connecting gateway, approval, Toolshed, and provider audit records.
This preserves separate security boundaries: the gateway controls access,
Agentic Firewall controls content, Toolshed controls discovery and dispatch,
and each provider retains native least privilege.
## Security model
Toolshed treats its registry as reviewed control-plane configuration. Agents
cannot add providers, change schemas, alter allowlists, or supply URLs.
The trusted identity headers are safe only behind an enforcer that strips
caller values and recreates them after authentication. The supplied Kubernetes
NetworkPolicy restricts Toolshed ingress to the example MCP enforcer labels.
For stronger production identity, add mTLS or service-mesh workload identity.
Agentic Firewall modes are:
- `disabled`: no inspection dependency;
- `audit`: record inspection failures and non-allow decisions without blocking;
- `enforce`: fail closed on inspection outage or any non-allow decision.
See [SECURITY.md](SECURITY.md) before deployment. The checks completed for this
public baseline are recorded in [SECURITY-REVIEW.md](SECURITY-REVIEW.md).
Deployment patterns for VPN/private endpoints, egress gateways, and mTLS are in
[Secure external connectivity](docs/SECURE-EXTERNAL-CONNECTIVITY.md).
## Scaling
Toolshed is stateless for MCP request processing and uses Streamable HTTP in
stateless JSON-response mode. The Kubernetes example starts three replicas and
includes an HPA, topology spreading, health probes, and a PodDisruptionBudget.
For multiple replicas, configure Redis so per-agent/per-tool quotas are shared.
The in-memory limiter is for a single development process only. Concurrency
semaphores remain per replica, so combine them with provider-side quotas or a
distributed concurrency control when a provider has a strict global limit.
At larger scale:
- partition high-risk or high-volume providers into separate Toolshed pools;
- autoscale on request latency, active work, provider saturation, and CPU;
- apply per-agent, per-tool, per-provider, and global budgets;
- use queues and job IDs for long-running or asynchronous operations;
- keep session state external if stateful MCP sessions are introduced;
- route external providers through controlled egress; and
- scale Agentic Firewall, Redis, gateway enforcers, and providers independently.
## Quick start
Requirements:
- Python 3.11 or newer; or
- Docker with Docker Compose.
Python development environment:
```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[test,scale]"
.\.venv\Scripts\python.exe -m pytest -q
```
Docker demonstration:
```powershell
Copy-Item .env.example .env
docker compose up --build
```
The Compose demonstration requires trusted identity headers because it models
the production boundary. It defaults Agentic Firewall to `audit`, allowing the
synthetic providers to run when no local inspection service is available.
Never expose this demonstration to an untrusted network.
## Configuration
| Variable | Default | Purpose |
| --- | --- | --- |
| `TOOLSHED_REGISTRY` | `config/tool-registry.json` | Reviewed provider and tool registry |
| `TOOLSHED_ENVIRONMENT` | `development` | Enables production endpoint validation when set to `production` |
| `TOOLSHED_REQUIRE_IDENTITY` | `true` | Requires trusted identity headers on MCP routes |
| `CONTENT_FIREWALL_URL` | `http://content-firewall:8080` | Agentic Firewall-compatible service base URL |
| `CONTENT_FIREWALL_MODE` | `enforce` | `disabled`, `audit`, or `enforce` |
| `REDIS_URL` | unset | Enables shared rate-limit counters |
| `EXTERNAL_MCP_NETWORK_MODE` | `blocked` in production; `direct_demo` otherwise | Remote MCP route: `blocked`, `private`, `proxy`, or local demo only |
| `EXTERNAL_MCP_PRIVATE_HOST_SUFFIXES` | unset | Comma-separated private DNS suffix allowlist for `private` mode |
| `EXTERNAL_MCP_PROXY_URL` | unset | Single explicit HTTPS gateway origin for `proxy` mode |
| `EXTERNAL_MCP_CA_BUNDLE` | system trust | Optional private CA bundle |
| `EXTERNAL_MCP_CLIENT_CERT` | unset | Optional mTLS client certificate; requires key |
| `EXTERNAL_MCP_CLIENT_KEY` | unset | Optional mTLS private key; requires certificate |
Provider credentials use environment-variable names declared by `auth_env` in
the registry. Store their values in a secret manager or workload secret; never
commit them.
## Add a provider
1. Deploy the provider in its own appropriate network and trust boundary.
2. Add its fixed endpoint, protocol transport, explicit `network_scope`,
timeout, concurrency, and optional credential environment-variable name to
`config/tool-registry.json`. External scope applies the secure route to HTTP
tools and remote MCP servers alike.
3. Add reviewed public tool names, remote names, schemas, risk labels, quotas,
output limits, tags, and explicit agent client-ID allowlists.
4. Permit only the required destination through controlled egress.
5. Test allow, deny, malformed input, malformed output, timeout, oversized
output, inspection block, and provider-unavailable paths.
Do not automatically import provider-supplied descriptions or schemas into a
production registry. A compromised provider must not be able to silently add a
tool or change its contract.
## Kubernetes deployment
The files under `deployments/kubernetes` are demonstration resources. Replace
all `REPLACE_ME` images, example identities, internal service names, registry
data, environment values, and secret references.
The `mcp-service` name and port match the upstream expected by the companion
Kong example. If that Service already exists, retain the existing Service and
apply only the compatible Deployment and supporting resources.
The Agentic Firewall integration uses that project's actual
`app.kubernetes.io/name: ai-content-firewall` workload label and expects its
Service in an `agentic-firewall` namespace. Change the namespace value if you
deploy one of the project's tenant overlays. The base egress policy also pins
the demo providers and optional Redis dependency by exact workload labels and
ports.
`egress-proxy.example.yaml` and `private-tunnel-egress.example.yaml` are
mutually exclusive starting profiles. They add narrow routes to the default
deny; they do not deploy a VPN or gateway. The platform team must supply that
infrastructure and corresponding provider-side restrictions.
## Project layout
```text
toolshed/ MCP server and enforcement runtime
config/tool-registry.json reviewed example registry
providers/internal_status/ separate internal HTTP example
providers/external_status/ separate remote MCP example
deployments/kubernetes/ demonstration scaling and isolation resources
docs/ production connectivity guidance
tests/ behavior and security regression tests
```
## Verification
```powershell
.\scripts\Verify.ps1
```
The verification gate runs tests, Ruff, Bandit, dependency consistency and
vulnerability checks, YAML parsing, Dockerfile checks, and secret-signature
scanning. A Docker image build and image scan remain release-environment checks
when no daemon is available.
The main runtime image installs from `requirements.lock` with hash checking.
Regenerate it after an intentional dependency change with:
```powershell
.\.venv\Scripts\pip-compile.exe --generate-hashes --extra scale --output-file requirements.lock pyproject.toml
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues