Skip to main content
The @radaros/transport package provides createAgentRouter() to generate a fully-featured Express router with endpoints for all your agents, teams, and workflows.

Installation

npm install @radaros/transport express

Quick Start

Explicit wiring

Pass agents, teams, and workflows by name:
import express from "express";
import { Agent, openai } from "@radaros/core";
import { createAgentRouter } from "@radaros/transport";

const assistant = new Agent({
  name: "assistant",
  model: openai("gpt-4o"),
  instructions: "You are a helpful assistant.",
});

const app = express();
app.use(express.json());

const router = createAgentRouter({
  agents: { assistant },
});

app.use("/api", router);
app.listen(3000);

Auto-discovery (zero-wiring)

Agents, teams, and workflows auto-register into a global registry when instantiated. The transport layer reads from this registry dynamically — entities created after the server starts are immediately available.
import express from "express";
import { Agent, openai } from "@radaros/core";
import { createAgentRouter } from "@radaros/transport";

const app = express();
app.use(express.json());
app.use("/api", createAgentRouter());  // no agents/teams/workflows needed

// Agents created anywhere auto-register and become available
new Agent({ name: "assistant", model: openai("gpt-4o") });
new Agent({ name: "analyst", model: openai("gpt-4o-mini") });

app.listen(3000);
// POST /api/agents/assistant/run  ✓
// POST /api/agents/analyst/run    ✓
You can also pass a mixed array via serve:
const router = createAgentRouter({
  serve: [assistant, analyst, researchTeam, pipeline],
});
This creates the following endpoints:
MethodPathDescription
POST/api/agents/:name/runRun agent, return JSON response
POST/api/agents/:name/streamStream response via Server-Sent Events
POST/api/teams/:name/runRun team
POST/api/teams/:name/streamStream team via SSE
POST/api/workflows/:name/runRun workflow
GET/api/agentsList registered agents with metadata
GET/api/teamsList registered teams
GET/api/workflowsList registered workflows
GET/api/registryList all registered names
GET/api/toolsList available tools (when toolkits/toolLibrary configured)
GET/api/tools/:nameGet single tool detail
GET/api/admin/mcpList MCP servers (when admin enabled)
POST/api/admin/mcpAdd + connect an MCP server
GET/api/admin/toolkitsList toolkit catalog

RouterOptions

agents
Record<string, Agent>
Map of named agents to expose. Each agent gets /agents/:name/run and /agents/:name/stream endpoints.
teams
Record<string, Team>
Map of named teams. Each gets /teams/:name/run and /teams/:name/stream endpoints.
workflows
Record<string, Workflow>
Map of named workflows. Each gets /workflows/:name/run endpoint.
serve
Servable[]
Mixed array of Agent, Team, and Workflow instances. Automatically classified and registered. An alternative to passing agents, teams, and workflows separately.
registry
Registry | false
Controls live auto-discovery. Defaults to the global registry (all auto-registered entities are available). Pass a custom Registry instance, or false to disable auto-discovery and only serve explicitly passed entities.
middleware
RequestHandler[]
Express middleware applied to all routes (e.g., auth, rate limiting).
swagger
SwaggerOptions
Enable Swagger UI. See Swagger docs.
fileUpload
boolean | FileUploadOptions
Enable multipart file upload for multi-modal input. See File Upload docs.
toolkits
Toolkit[]
Toolkit instances whose tools are exposed via GET /tools. Useful for UI tool discovery.
toolLibrary
Record<string, ToolDef>
Named tools exposed via GET /tools. Merged with toolkit tools (explicit entries take precedence).
admin
boolean | { mcpManager?: MCPManager; middleware?: any[] }
Enable admin routes under /admin for managing MCP servers and the toolkit catalog at runtime. Pass true to use defaults, or provide a shared MCPManager instance and authentication middleware. See MCP Admin.In production, admin routes require authentication middleware — the server will throw an error at startup if none is provided.

Request Format

JSON Request

curl -X POST http://localhost:3000/api/agents/assistant/run \
  -H "Content-Type: application/json" \
  -d '{"input": "What is TypeScript?"}'

With Session

curl -X POST http://localhost:3000/api/agents/assistant/run \
  -H "Content-Type: application/json" \
  -d '{"input": "What was my last question?", "sessionId": "user-123"}'

With API Key

curl -X POST http://localhost:3000/api/agents/assistant/run \
  -H "Content-Type: application/json" \
  -H "x-openai-api-key: sk-your-key" \
  -d '{"input": "Hello"}'

Response Format

Run Response

{
  "text": "TypeScript is a typed superset of JavaScript...",
  "toolCalls": [],
  "usage": {
    "promptTokens": 25,
    "completionTokens": 150,
    "totalTokens": 175,
    "providerMetrics": {
      "prompt_tokens": 25,
      "completion_tokens": 150,
      "total_tokens": 175,
      "prompt_tokens_details": { "cached_tokens": 0 },
      "completion_tokens_details": { "reasoning_tokens": 0 }
    }
  },
  "timeToFirstTokenMs": 320,
  "runId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "agentName": "assistant",
  "sessionId": "",
  "userId": "",
  "model": "gpt-4o",
  "modelProvider": "openai",
  "status": "completed",
  "createdAt": 1772453994305,
  "responseId": "chatcmpl-abc123",
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    { "role": "user", "content": "What is TypeScript?" }
  ],
  "metrics": {
    "inputTokens": 25,
    "outputTokens": 150,
    "totalTokens": 175,
    "timeToFirstTokenMs": 320,
    "durationMs": 1234
  },
  "structured": null,
  "durationMs": 1234
}

Structured Output Response

When an agent has structuredOutput configured, the structured field contains the parsed and validated JSON object:
{
  "text": "{\"summary\":\"AI agents have advanced...\",\"keyPoints\":[...],\"confidence\":0.85}",
  "toolCalls": [],
  "usage": {
    "promptTokens": 82,
    "completionTokens": 129,
    "totalTokens": 211
  },
  "structured": {
    "summary": "AI agents have advanced significantly in 2026...",
    "keyPoints": [
      "Diagnostic accuracy improved by 20% with AI imaging tools.",
      "Customer service bots handle 70% of inquiries."
    ],
    "confidence": 0.85
  },
  "durationMs": 3026
}

Stream Response (SSE)

curl -X POST http://localhost:3000/api/agents/assistant/stream \
  -H "Content-Type: application/json" \
  -d '{"input": "Tell me a joke"}'
data: {"type":"text","text":"Why"}
data: {"type":"text","text":" did"}
data: {"type":"text","text":" the"}
...
data: {"type":"finish","finishReason":"stop"}

List Endpoints

When auto-discovery is enabled (default), the router exposes list endpoints with rich metadata:
# List all agents with model, provider, tools
curl http://localhost:3000/api/agents
[
  {
    "name": "assistant",
    "model": "gpt-4o",
    "provider": "openai",
    "tools": ["calculate"],
    "hasStructuredOutput": false
  }
]
# List all registered names
curl http://localhost:3000/api/registry
{
  "agents": ["assistant", "analyst"],
  "teams": ["research"],
  "workflows": ["pipeline"]
}

Full Example with Multiple Agents

import express from "express";
import { Agent, openai, google, defineTool } from "@radaros/core";
import { createAgentRouter } from "@radaros/transport";
import { z } from "zod";

const calculator = defineTool({
  name: "calculate",
  description: "Evaluate a math expression",
  parameters: z.object({ expression: z.string() }),
  execute: async ({ expression }) => String(eval(expression)),
});

// Agents auto-register into the global registry
const assistant = new Agent({
  name: "assistant",
  model: openai("gpt-4o"),
  instructions: "You are a helpful assistant.",
  tools: [calculator],
});

const analyst = new Agent({
  name: "analyst",
  model: openai("gpt-4o-mini"),
  instructions: "You analyze data and provide insights.",
  structuredOutput: z.object({
    summary: z.string(),
    sentiment: z.enum(["positive", "negative", "neutral"]),
    confidence: z.number(),
  }),
});

const vision = new Agent({
  name: "vision",
  model: google("gemini-2.5-flash"),
  instructions: "You analyze images and describe what you see.",
});

const app = express();
app.use(express.json());

// Auto-discovery: no need to pass agents explicitly
const router = createAgentRouter({
  swagger: {
    enabled: true,
    title: "My AI API",
    description: "Multi-agent API powered by RadarOS",
  },
  fileUpload: true,
});

app.use("/api", router);

app.listen(3000, () => {
  console.log("API running on http://localhost:3000");
  console.log("Swagger UI at http://localhost:3000/api/docs");
});

Adding Custom Middleware

const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) return res.status(401).json({ error: "Unauthorized" });
  next();
};

const router = createAgentRouter({
  agents: { assistant },
  middleware: [authMiddleware],
});

Error Handling

import { errorHandler } from "@radaros/transport";

app.use("/api", router);
app.use(errorHandler);
The errorHandler middleware catches errors and returns structured JSON responses:
{
  "error": "Agent \"unknown\" not found"
}
In production (NODE_ENV=production), 5xx errors return a generic "Internal server error" message. Stack traces and internal details are never exposed to clients.

Security

RadarOS includes several built-in security measures for the Express transport layer:

Rate Limiting

createAgentRouter() includes a built-in IP-based rate limiter. The rate limiter’s internal state is periodically cleaned (every 60 seconds) to prevent unbounded memory growth from large numbers of unique IPs.

Admin Route Authentication

In production (NODE_ENV=production), mounting admin routes without authentication middleware will throw an error at startup — not just a warning. Always pass auth middleware when enabling admin routes:
const router = createAgentRouter({
  admin: {
    middleware: [authMiddleware],
  },
  middleware: [authMiddleware],
});

Request Logging

The requestLogger middleware sanitizes URL paths to prevent log injection. Control characters and newlines are stripped before logging.

JSON Body Limits

The A2A server applies a 1MB JSON body size limit to prevent large-payload DoS attacks. See the Security page for a full overview of all security protections.