The CLI is published on npm (npm i -g @aex-lang/cli). APIs are experimental and may change before v1.0. Roadmap →
Skip to content

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:

  1. Is it allowed? — tool must be in use and not in deny
  2. Does it need confirmation? — any confirm before <tool> step triggers an approval gate
  3. Should it be blocked? — denied tools are rejected immediately

Installation ​

bash
npm install @aex-lang/mcp-gateway

Quick Start ​

ts
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:

aex
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, and db.schema
  • Blocks db.drop, db.truncate, and anything under admin.*
  • Requires human confirmation before any db.write call

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:

ts
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 ​

aex
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 endpoints

2. Create a policy ​

json
{
  "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 ​

ts
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 ​

ts
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:

bash
aex run tasks/api-proxy.aex \
  --policy tasks/api-proxy.policy.json \
  --inputs tasks/api-proxy.inputs.json

Policies 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:

ts
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 use list — the contract doesn't declare it
  • Tool matches deny pattern — deny network.* blocks network.fetch
  • Confirmation denied — user rejected the confirm before gate

See Also ​

Prompts are not permissions. Plans are not contracts.