MCP Gateway
The @aex-lang/mcp-gateway package sits between an MCP client and an MCP server, using an AEX contract to decide which tool calls are allowed, which require confirmation, and which are blocked.
Overview
MCP Client ---> AEX Gateway ---> MCP Server
|
v
.aex contract
(permissions)The gateway reads an .aex file and answers three questions for every incoming tool call:
- Is it allowed? — tool must be in
useand not indeny - Does it need confirmation? — any
confirm before <tool>step triggers an approval gate - Should it be blocked? — denied tools are rejected immediately
Installation
npm install @aex-lang/mcp-gatewayQuick Start
import { AEXMCPGateway } from "@aex-lang/mcp-gateway";
const gateway = new AEXMCPGateway("tasks/db-access.aex");
// Check before forwarding a tool call
if (!(await gateway.allows("db.query"))) {
throw new Error("db.query is not allowed by the contract.");
}
if (await gateway.requiresConfirmation("db.write")) {
// Surface a human approval prompt before forwarding
const approved = await promptUser("Allow db.write?");
if (!approved) throw new Error("db.write denied by operator.");
}
// Safe to forward the call to the MCP server
const result = await mcpServer.call("db.query", args);Writing a Gateway Contract
A gateway contract defines the permissions boundary for an MCP server. Here's one that protects a database server:
task db_gateway v0
goal "Allow read queries but gate all writes behind confirmation."
use db.query, db.write, db.schema
deny db.drop, db.truncate, admin.*
confirm before db.write
return { status: "gateway ready" }This contract:
- Allows
db.query,db.write, anddb.schema - Blocks
db.drop,db.truncate, and anything underadmin.* - Requires human confirmation before any
db.writecall
API Reference
new AEXMCPGateway(taskPath: string)
Create a gateway from an .aex contract file. The contract is parsed lazily on first use and cached.
gateway.allows(toolName: string): Promise<boolean>
Returns true if the tool is in the contract's use list and not in the deny list. Supports wildcard patterns — network.* matches network.fetch, network.post, etc.
gateway.requiresConfirmation(toolName: string): Promise<boolean>
Returns true if any confirm before <tool> step in the contract matches the tool name.
gateway.summary(): Promise<GatewaySummary>
Returns the full permission sets:
interface GatewaySummary {
allowedTools: string[]; // from `use`
deniedTools: string[]; // from `deny`
confirmTools: string[]; // from `confirm before` steps
}Use this to build audit dashboards or permission matrices.
End-to-End Example
1. Create the contract
task api_proxy v0
goal "Proxy API calls with rate limiting and access control."
use api.get, api.post, api.list
deny api.delete, admin.*
need api_key: str
budget calls=50
confirm before api.post
do api.list() -> endpoints
return endpoints2. Create a policy
{
"allow": ["api.get", "api.post", "api.list"],
"deny": ["api.delete", "admin.*"],
"require_confirmation": ["api.post"],
"budget": { "calls": 50 }
}3. Wire the gateway into your MCP proxy
import { AEXMCPGateway } from "@aex-lang/mcp-gateway";
const gateway = new AEXMCPGateway("tasks/api-proxy.aex");
const summary = await gateway.summary();
console.log("Allowed:", summary.allowedTools);
console.log("Denied:", summary.deniedTools);
console.log("Confirm:", summary.confirmTools);
// In your MCP request handler:
async function handleToolCall(toolName: string, args: unknown) {
if (!(await gateway.allows(toolName))) {
return { error: `Tool "${toolName}" is blocked by contract.` };
}
if (await gateway.requiresConfirmation(toolName)) {
const approved = await requestApproval(toolName);
if (!approved) {
return { error: `Tool "${toolName}" denied by operator.` };
}
}
return await forwardToMCPServer(toolName, args);
}4. Check the summary for audit
const summary = await gateway.summary();
// {
// allowedTools: ["api.get", "api.post", "api.list"],
// deniedTools: ["api.delete", "admin.*"],
// confirmTools: ["api.post"]
// }Combining with Runtime Policies
The gateway reads permissions from the .aex file. For additional runtime constraints (path restrictions, budget caps), pass a policy to aex run:
aex run tasks/api-proxy.aex \
--policy tasks/api-proxy.policy.json \
--inputs tasks/api-proxy.inputs.jsonPolicies can extend base policies with the extends field for org-wide defaults.
Error Handling
When the gateway blocks a tool call, surface a clear error to the client:
async function handleToolCall(toolName: string, args: unknown) {
if (!(await gateway.allows(toolName))) {
return {
error: `Tool "${toolName}" is blocked by contract.`,
code: "AEX_DENIED",
};
}
if (await gateway.requiresConfirmation(toolName)) {
const approved = await requestApproval(toolName);
if (!approved) {
return {
error: `Tool "${toolName}" denied by operator.`,
code: "AEX_CONFIRMATION_DENIED",
};
}
}
return await forwardToMCPServer(toolName, args);
}Common scenarios:
- Tool not in
uselist — the contract doesn't declare it - Tool matches
denypattern —deny network.*blocksnetwork.fetch - Confirmation denied — user rejected the
confirm beforegate
See Also
- Policy Reference — runtime policy enforcement rules
- OpenAI Agents SDK — programmatic adapter for OpenAI agents
- CLI Reference — full list of CLI flags
- Meta-Tools Reference — checkpoint, resume, list, review via proxy