secure-mcp-agent-starter
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@secure-mcp-agent-startershow me tool calls waiting for my approval"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
secure-mcp-agent-starter
A runnable reference implementation of a production-hardened Model Context Protocol server and the gateway that sits between an AI agent and its tools.
It is the code behind NubisCore's article Securing AI Agents in Production: Identity, MCP, and Least-Privilege Tool Access. The core controls the article describes are implemented here, tested, and demonstrated end to end by a sample client; What this is not lists the ones left to your platform. Clone it, run three commands, and watch a poisoned document get stripped, an irreversible action get held for a human, a replayed token get rejected, and a kill switch take effect mid-session.
There is no LLM in the loop on purpose. The point is what the gateway does regardless of what a model asks for.
What it implements
Control | Where | What it closes |
OAuth 2.1 resource server with RFC 9728 protected resource metadata |
| Clients guessing which authorization server to use |
Audience-restricted token verification (RFC 8707 resource indicator) |
| The confused deputy: a token for one server replayed against another |
Two identities per call: delegating user and agent workload ( |
| Shared long-lived API keys with no attribution |
Single-audience token check |
| A token valid for several servers replayed against this one |
Purpose-bound tool manifest: only the agent's declared tools are registered for its session |
| Tools an agent was never meant to have, even when its token has the scope |
Whole-manifest hashing and pinning (tools and agent bindings) |
| Tool poisoning, rug pulls, and quietly widened purposes |
Row-level authorization against the delegating user |
| An agent reading every user's data through one user's request |
Session binding on every request, including SSE and session close |
| Driving or terminating one identity's session with another's token |
Containment: tool-call cap, purpose binding, value ceiling, PII-then-egress chain break, mutation caps, all carried across sessions |
| Runaway loops and read-then-exfiltrate sequences, including via a fresh session |
Human-in-the-loop gate on the concrete, schema-valid parameters, single use, approved by a separate identity |
| Approving the agent's prose, or the agent approving itself |
Per-tool, per-identity rate limits |
| Two hundred refunds an hour from an agent that normally does two |
File-backed kill switch evaluated on every call, failing closed |
| Waiting for a deploy while an agent misbehaves at 02:00 |
Untrusted-content envelope applied centrally by the gateway, with Unicode-aware carrier stripping |
| Indirect prompt injection via zero-width, TAG-block, fullwidth, or HTML-comment carriers |
Idle-session expiry and per-subject session cap |
| A valid token exhausting memory by opening sessions |
Structured audit event per call, with delegation chain and content provenance |
| "What did it do, under whose authority, and why?" |
Hardened Kubernetes deployment: default-deny egress, no metadata endpoint, restricted PSA, Gatekeeper constraint |
| Exfiltration routes and credential theft from the runtime |
Related MCP server: mcp-tool-gateway
Quick start
Requires Node 22+ and pnpm.
pnpm install
cp dev/kill-switch.example.json dev/kill-switch.json
pnpm dev:issuer # terminal 1: development-only token issuer on :3000
pnpm dev:server # terminal 2: the MCP server on :3001
pnpm dev:client # terminal 3: the walkthroughThe client walks through the controls step by step and prints what happened at each one. The server terminal prints one JSON audit event per tool call. pnpm test runs the same flow as an integration test, plus unit tests for each control.
What the walkthrough shows
Discovery of the resource server through its protected resource metadata.
A delegated token for a user, acting through a declared agent, with this server as the audience.
The tool list, annotated with read-only, mutating, or destructive hints from the manifest. A second agent with the same scopes but a narrower purpose sees a shorter list.
Reading the user's own ticket succeeds. Reading another user's ticket with the same scope returns not found.
A knowledge base search returns a poisoned document. The hidden instruction is stripped, the result is wrapped in a provenance envelope, and the event is flagged as suspicious.
A comment is posted with a
triggering_content_sourceso the audit trail records which document drove the decision.A refund is requested. It is contained, not executed, and the response carries the concrete parameters and an approval key.
The agent tries to approve itself with the operator scope on its own token and is refused. An operator with a separate identity approves exactly those parameters.
The retry succeeds once.
A retry with different parameters is contained again. Approvals do not carry over. Then, because the session read PII earlier, an outbound notification is contained, and opening a fresh session does not reset that. Another user's token cannot terminate the session.
A token minted for a different resource server is rejected with an audience error.
A valid token for an agent that is not declared in the manifest is rejected.
Want to try to break it yourself? TESTING.md is a hands-on checklist with the expected result for each attempt.
How a call flows
agent ──bearer token──▶ /mcp
│
├─ verify signature, issuer, single audience, expiry (auth/verify-token.ts)
├─ resolve session; user, agent, and workload must match (server.ts)
├─ scope check against the tool's required_scope
├─ containment: kill switch, caps, purpose, value ceiling,
│ PII-then-egress, approval gate (policy/containment.ts)
├─ rate limit per tool per user (policy/rate-limit.ts)
├─ handler with row-level auth (tools/handlers.ts)
├─ gateway wraps untrusted output, text and structured (untrusted/envelope.ts)
└─ emit audit event, allowed or not (audit/events.ts)Nothing in that sequence depends on the model cooperating. That property is what makes it a control rather than a suggestion.
The tool manifest
tools/manifest.yaml is the single file that declares the agent's entire capability surface. Review it like you would review an IAM policy.
agents:
agent-support-triage:
tools: [get_ticket, search_kb, post_ticket_comment, notify_customer, issue_refund]
tools:
- name: issue_refund
purpose: Issue a refund against a completed order
required_scope: billing:refund
mutating: true
reversible: false
authorization: delegated
confirmation: human_in_the_loop # required for irreversible tools; the loader rejects the manifest otherwise
max_value_cents: 50000
rate_limit: 5/hour
parameters:
order_id: { type: string, pattern: "^ORD-[0-9]{5}$" }
amount_cents: { type: integer, min: 1, max: 50000 }
reason: { type: string, max_length: 500 }Parameter declarations become the tool's input schema, so a malformed argument never reaches the handler. Fields marked redact: true are blanked in audit events. touches_pii and external_egress feed the chain-breaking rule, which is keyed by (user, agent), survives a new session, and reaches sessions that were already open: the gateway re-reads the (user, agent) ledger before every call. untrusted_output makes the gateway strip and wrap the result centrally, so a handler cannot forget. The agents block is purpose binding: only the tools listed for a client_id are registered for its session, whatever scopes its token carries.
pnpm manifest:hash prints the hash of the whole file, tool definitions and agent bindings alike. Set MANIFEST_HASH_PIN to that value and a changed manifest refuses to start. deploy/k8s/manifest.yaml is the same file with the deployed resource, and deploy/k8s/kustomization.yaml pins its hash; pnpm test fails if the tools or agent bindings drift from tools/manifest.yaml, if resource differs from the deployment's MCP_CANONICAL_URI, or if the pin is stale.
Configuration
All configuration is environment variables. See .env.example.
Variable | Purpose |
| This server's canonical identifier. Tokens must carry it in |
| Your authorization server. The stub in |
| Path to the manifest. |
| Refuse to start if the manifest hash differs. Required when |
| JSON file re-read on every call. Mount it from a ConfigMap in Kubernetes. Missing or malformed fails closed, and startup refuses to run without it. |
| Per-session cap on tool calls, counted by the gateway. |
| Irreversible actions per (user, agent) per hour, counted across sessions. |
| Caps on the agent's self-reported loop index and token spend, sent as |
The server holds no long-lived secrets. It verifies tokens with the authorization server's public keys and nothing else.
Deploying
kubectl apply -k deploy/k8s/gatekeeper # cluster-scoped policy objects
kubectl apply -k deploy/k8s # namespace, gateway, network policies, configdeploy/k8s/ contains:
a namespace under the
restrictedPod Security level,a single-replica deployment that runs non-root with a read-only root filesystem, no capabilities, no service account token, and the manifest and kill switch mounted from a ConfigMap,
a default-deny egress NetworkPolicy for pods labelled
app.kubernetes.io/component=agent-runtime, allowing only DNS, the gateway, and an egress proxy, and a gateway policy that allows only DNS and the egress proxy, so neither the public internet nor the cloud instance metadata endpoint is reachable directly,an OPA Gatekeeper template and constraint that enforce
drop: [ALL], read-only root, non-root, and a component label the network policies actually select on.
Adjust the egress proxy namespace and labels in networkpolicy.yaml to your environment; without a proxy the gateway cannot fetch the authorization server's keys, which is the intended failure mode.
The kill switch is the ConfigMap. On-call flips it with kubectl edit, and once the kubelet refreshes the mounted file (typically within a minute or two) the next tool call from the disabled agent is contained. For faster propagation, back FlagStore with a feature-flag service. Re-running kubectl apply -k resets the flags to deploy/k8s/kill-switch.json, so clear the switch in Git too, or leave it set there during an incident. Exercise this in a game day before you need it. Note that disabled_versions keys on the version the MCP client reports about itself, which is useful for halting a bad rollout but is client-controlled; the per-agent and global switches key on the verified client_id.
What this is not
Not a complete authorization server.
dev/issuer.tsmints tokens for anyone who asks. It exists so the starter runs on a laptop. Delete it from any real deployment.Not multi-replica ready as-is. Sessions, rate limits, approvals, and the cross-session ledger are in memory, which is why the deployment ships with one replica. Back them with Redis or your store of choice before scaling out. The interfaces are small and in
src/policy/.Not the whole kill switch. It contains the disabled agent's next tool call. Revoking its tokens and draining open sessions belong to your authorization server and platform runbook.
Not a circuit breaker. Anomalies such as tool-call spikes, clustered authorization failures, and first-time tool use are emitted as audit events for your SIEM to alert on; nothing here trips automatically on them.
Not a client-side gateway for third-party MCP servers. This is the server side. Allowlisting and hash-pinning the third-party servers your agents connect to happens in the client or an egress gateway.
Not enforcing workload identity beyond binding. The
actclaim is verified, bound to the session, and recorded in every audit event, but no policy here says "this workload may not call that tool". Add that incheck()when you have a workload identity system to key it on.Not a defence against prompt injection. The envelope reduces the probability. The containment layer bounds the consequence. Neither is complete, and anyone selling you a complete one is overselling.
Not a product. It is a reference you fork, keep the shape of, and replace the sample handlers in.
Production checklist
Before this shape goes live with real tools behind it:
Real authorization server issuing audience-restricted tokens with the
resourceparameter on both the authorization and token requests.Workload identity for the agent from SPIFFE/SPIRE, IRSA, GKE workload identity, or managed identity, carried in the
actclaim.Every handler enforces row-level authorization against
identity.subject.MANIFEST_HASH_PINset from the hash you reviewed and approved, anddeploy/k8s/manifest.yamlin sync.Sessions, approvals, rate limits, and the ledger moved to a shared store before running more than one replica.
Audit events shipped to your SIEM with alerts on clustered authorization failures, first-time tool use, approval gates hit at unusual frequency, and manifest hash mismatches.
Kill switch tested in a game day, by the people who will be on call.
Egress proxy with an FQDN allowlist in front of any tool that reaches the internet.
Code execution, if any, moved out of the container into gVisor, Firecracker, or a separate cluster.
Project layout
src/
auth/verify-token.ts token verification, delegation chain
tools/manifest.ts manifest schema, loader, hash
tools/registry.ts the gateway: every tool call passes through here
tools/handlers.ts sample business logic with row-level auth
policy/containment.ts the check() evaluated before every call
policy/session.ts per-session state, approvals, cross-session ledger
policy/kill-switch.ts file-backed flags, read on every call, fail closed
policy/rate-limit.ts sliding window per tool per identity
untrusted/envelope.ts strip hidden carriers, wrap with provenance
audit/events.ts one structured event per call
server.ts Express app: metadata, /mcp, /admin/approvals
dev/issuer.ts development-only token issuer
examples/client.ts the walkthrough
deploy/k8s/ kustomize base; deploy/k8s/gatekeeper for cluster-scoped policy
test/ unit tests per control plus an end-to-end flowBackground
The rationale for each control, the threat model, and a 90-day rollout order are in the article this repository accompanies: Securing AI Agents in Production. Related reading from the same series: Building Production Agents on Amazon Bedrock and Securing Kubernetes with Network Policies and OPA Gatekeeper.
Maintained by NubisCore. If you are putting agents into production and want a second pair of eyes on the identity and containment design, get in touch.
License
Apache 2.0. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Give AI agents identity, scoped access, trusted context, and verifiable actions through MCP.
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
Find, vet, and run MCP tools through a secure audited gateway with prompt-injection risk scoring
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to discover and execute tools via a secure MCP server with JWT authentication, RBAC, rate limiting, and audit logging.1MIT
- AlicenseNot gradedqualityDmaintenanceA secure tool-execution plane for agentic AI that enforces JWT authentication, rate limiting, prompt-injection inspection, and audit logging, while ingesting downstream OpenAPI endpoints as MCP tools.MIT
- AlicenseNot gradedqualityCmaintenanceGoverned MCP server for bank-grade agent tool access with RBAC, PII redaction, rate limiting, and audit logging.MIT

evav-gatewayofficial
AlicenseNot gradedqualityBmaintenanceGoverned MCP gateway that lets AI agents call tools with policy enforcement, prompt-injection screening, a kill-switch, and tamper-evident signed audit logs.Apache 2.0