Guardio is a control plane that sits between your AI Agent system and the external world. It catches and evaluates messages flowing to and from MCP tools and other APIs before they reach the real servers. You can enforce policies (allow, block, sanitize), require approval, simulate MCP responses and observe activity—all through a plugin system.
| Connection type | Status | Notes |
|---|---|---|
| HTTP server | Supported | Guardio runs as an HTTP server; clients connect here. |
| MCP tool (upstream) | Supported | Proxying to MCP servers over HTTP/SSE. |
| stdio | On the way | Client transport. |
| Other APIs / transports | On the way | Extensible for more protocols. |
Today you run one Guardio instance that fronts all your external MCP tools and APIs (one proxy, many upstreams).
Scaffold a new project with config and optional plugins:
npx create-guardioYou will be prompted for:
- Guardio directory – e.g.
guardio-project(default) - Guardio HTTP port – e.g.
3939 - Storage and events – optional; needed for dashboard and policy state. Choose SQLite (in-memory by default, or file
guardio.sqlite) or PostgreSQL. - Example custom policy plugin? – optional; scaffolds
plugins/example - Install dashboard? – optional; adds
@guardiojs/dashboardand adashboardrun script
The scaffold creates empty servers by default. A commented example in guardio.config.ts shows how to add an MCP server (e.g. { name: "nuvei-docs", type: "url", url: "https://mcp.nuvei.com/sse" }). All built-in policy plugins (deny-tool-access, deny-regex-parameter) are included by default.
Then:
cd <guardio-directory>
npm install # or: pnpm install, yarn, bun install, etc.
npm run guardioPoint your AI Agent or MCP client at http://127.0.0.1:<port>. If you installed the dashboard, run pnpm run dashboard (or npm run dashboard) and point it at the same Guardio base URL.
A minimal Docker image is provided for the core Guardio HTTP server (package @guardiojs/guardio). Build it from the packages/guardio directory:
cd packages/guardio
docker build -t guardio .Run the container, mounting your guardio.config.* into the container and mapping the HTTP port (defaults to 3939 unless overridden in config or via env):
docker run --rm \
-p 3939:3939 \
-v "$(pwd)/guardio.config.ts:/config/guardio.config.ts:ro" \
guardio \
--config /config/guardio.config.tsThe container:
- Exposes port
3939by default (override withGUARDIO_HTTP_PORT/GUARDIO_HTTP_HOST). - Starts the Guardio CLI via
node bin/guardio.mjs(you can pass any CLI args after the image name).
AI Agents (MCP clients) connect to Guardio's HTTP server, not directly to the upstream MCP servers. Guardio is the single entry point.
- SSE (stream) – Connect to
http://<host>:<port>/{serverName}/ssefor the MCP SSE stream. Use the server name from your config (e.g.nuvei-docs→/nuvei-docs/sse). - Optional
x-agent-name– Send this header on the SSE connection to give the agent a human-readable name. If omitted, Guardio generates one. The connection is assigned an agent id used for policy scoping. - POST messages – Send JSON-RPC to
http://<host>:<port>/{serverName}/messages. You can sendx-agent-id(the id for the SSE connection) so policies can be applied per agent.
So: one Guardio URL base, multiple paths like /{mcp-tool}/sse and /{mcp-tool}/messages for each configured upstream.
In your config you define a servers array. Each entry has a name (unique, used in the URL path) and an url (the upstream MCP server's HTTP/SSE base URL). Guardio proxies:
- GET /{name}/sse – to the upstream SSE endpoint (and manages the stream).
- POST /{name}/messages – to the upstream after running policies (or returns a blocked result without forwarding).
So each "MCP tool" or upstream is one entry in servers; a single Guardio instance serves all of them.
Plugins extend Guardio's behavior. Types:
| Type | Role |
|---|---|
| Policy | Evaluate tools/call requests: allow, block, or modify arguments. Optional; no policies means all calls pass through. |
| Storage | Persist state (e.g. policy assignments, agent list). Used by built-in policy config and dashboard. |
| EventSink | Receive events for each processed request (e.g. ALLOWED/BLOCKED, tool name, policy). |
| EventSinkStore | Store and query events; used by the dashboard for activity views. |
Built-in plugins:
- Policy:
deny-tool-access,deny-regex-parameter(both are added by default when you scaffold withcreate-guardio) - Storage / EventSink / EventSinkStore:
sqliteorpostgres- sqlite:
config: { inMemory: true }(default) orconfig: { database: "guardio.sqlite" }for a file - postgres:
config: { connectionString: "postgresql://user:pass@host:5432/dbname" }or discretehost,port,user,password,database,ssl
- sqlite:
You register plugins in guardio.config.ts in the plugins array. Policy config for built-ins is typically managed at runtime (e.g. via the dashboard), not in the config file.
Use a path-based plugin: in config add an entry with path pointing to a directory that contains index.js or index.mjs (build from index.ts if needed).
Export a PolicyPluginDefinition with a factory function. This enables full dashboard integration: runtime configuration, multiple instances, config validation, and custom form widgets.
// plugins/my-policy/index.ts
import { z } from "zod";
import type {
PolicyPluginDefinition,
PolicyPluginInterface,
PolicyRequestContext,
PolicyResult,
PolicyPluginContext,
} from "@guardiojs/guardio";
// Config schema for dashboard validation
const configSchema = z.object({
maxLength: z.number().min(1).describe("Maximum argument length"),
blockedPatterns: z.array(z.string()).optional(),
});
type Config = z.infer<typeof configSchema>;
class MyPolicyPlugin implements PolicyPluginInterface {
readonly name = "my-policy";
constructor(
private config: Config,
private context?: PolicyPluginContext,
) {}
async evaluate(ctx: PolicyRequestContext): Promise<PolicyResult> {
// Access config: this.config.maxLength
// Access plugin storage: this.context?.pluginRepository
return { verdict: "allow" };
}
}
const definition: PolicyPluginDefinition = {
name: "my-policy",
factory: (config, context) => new MyPolicyPlugin(config as Config, context),
configSchema,
uiSchema: {
maxLength: { "ui:widget": "updown" },
},
};
export default definition;In config:
{ type: "policy", name: "my-policy", path: "./plugins/my-policy" }Custom plugins work exactly like built-in plugins:
- Create multiple instances with different configs via the dashboard
- Configs are stored in the database and validated against your schema
- Plugins receive a
PolicyPluginContextwith a scopedPluginRepositoryfor persisting plugin-specific state
If you chose "Add example custom policy plugin" when running npx create-guardio, see the generated plugins/example folder.
When a tools/call request hits Guardio (POST /{serverName}/messages):
- Guardio resolves which policy plugins apply (from storage, optionally scoped by agent and tool).
- Each policy's
evaluateis run with the tool name and arguments. - If any policy returns block, the call is not forwarded. Guardio responds with a success JSON-RPC result that includes a human-readable message and
_guardiometadata (so agent frameworks don't treat it as a fatal error). - If all policies allow, the request (with any modified arguments) is forwarded to the upstream MCP server and the response is proxied back.
Non–tools/call messages are forwarded without policy evaluation.
If EventSink plugins are configured, Guardio emits a GuardioEvent for each processed tools/call (both allowed and blocked). The event includes:
- decision –
ALLOWEDorBLOCKED - tool name, request id, agent id (if present)
- For blocks: policy name, code, reason
EventSinkStore (e.g. sqlite or postgres) persists these for the dashboard and for your own auditing.
Guardio supports Simulation Mode, which returns mocked tool responses instead of calling the real upstream MCP server. This is useful for testing and demos because policies still run first, but the upstream call is skipped.
- Global simulation: when enabled, all
tools/callrequests are simulated (regardless of per-tool settings). - Per-tool simulation: when global simulation is off, you can mark specific tools as simulated per MCP server + tool name.
- Per-request header:
X-Guardio-Mode: simulationcan simulate a single request when global simulation is off.
In the dashboard sidebar, open Testing → Simulation to:
- Toggle Global simulation on/off
- Configure Per-tool simulation per MCP server + tool
These settings are stored in the database (runtime settings) and can be changed without restarting Guardio.
- GET
/api/testing/simulation– returns current simulation settings - PUT
/api/testing/simulation– updates simulation settings
Example payload:
{
"globalSimulated": false,
"tools": [
{ "serverName": "docs", "toolName": "search", "simulated": true }
]
}When Simulation Mode is used for a tools/call, Guardio records simulation details in the event so the dashboard activity feed can render it (e.g. simulation.enabled and simulation.source).
- Config file:
guardio.config.ts(or pass--config <path>). It must export aGuardioConfigwithservers(array of{ name, type: "url", url }; can be empty) andplugins. The scaffold adds a commented example server in the config so you can uncomment and edit to add MCP upstreams. - Client (HTTP server): Optional
clientwithport(default3939) andhost(default127.0.0.1). Override withGUARDIO_HTTP_PORTandGUARDIO_HTTP_HOST. - Debug:
GUARDIO_DEBUG=1to log request/response flow.
Blocked tool calls return a success result with result.isError: true and result._guardio (version, requestId, timestamp, policyId, action). See the repo for the full response shape.
The dashboard is a Next.js web UI for Guardio. It lets you view activity (allowed/blocked tool calls), manage policies, and inspect agents and topology. You can add it when scaffolding with npx create-guardio (choose "Install dashboard?").
Apache-2.0