Gateway & Sessions
The Gateway manages multiple sessions, routes messages, and provides a method-based API for external access. It exposes a JSON-RPC-like protocol over WebSocket and HTTP/SSE that any client in any language can consume.
Creating a Gateway
import { createGateway, method } from "@agentick/gateway";
import { z } from "zod";
const gateway = createGateway({
apps: { myApp },
defaultApp: "myApp",
methods: {
chat: {
send: method({
description: "Send a chat message",
schema: z.object({
message: z.string(),
sessionId: z.string(),
}),
handler: async ({ message, sessionId }) => {
const ctx = Context.get();
const session = await ctx.session({ id: sessionId });
return session.send({
messages: [{ role: "user", content: message }],
}).result;
},
}),
},
},
});Session Management
The gateway creates and manages sessions on demand:
// Sessions are created via the app
const session = await app.session({ id: "user-123" });
// Each session has its own component tree, state, and timeline
await session.send({ messages: [...] });Custom Methods
Gateway methods are typed RPC endpoints with schema validation, role guards, and ALS context:
methods: {
namespace: {
methodName: method({
description: "Do something useful",
schema: z.object({ /* params */ }),
response: z.object({ /* response */ }),
roles: ["admin"],
handler: async (params) => {
const ctx = Context.get();
return { result: "value" };
},
}),
},
}Both schema (params) and response accept Zod 3, Zod 4, or any Standard Schema. They appear as JSON Schema in the protocol's schema method.
Protocol & Schema Discovery
The gateway's schema method returns the complete protocol contract at runtime — every method with full JSON Schema for params and response, every event type with its category, and every error code. A client in any language can build a full SDK from this single response.
See the Gateway Protocol reference for the full specification.
Plugins
Plugins extend the gateway with methods, HTTP routes, and event handlers:
const plugin: GatewayPlugin = {
id: "my-plugin",
async initialize(ctx) {
// Register RPC methods
ctx.registerMethod(
"analyze",
method({
schema: z.object({ text: z.string() }),
response: z.object({ sentiment: z.number() }),
handler: async (params) => ({ sentiment: 0.8 }),
}),
);
// Mount HTTP routes
ctx.registerRoute("/webhook", async (req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
});
ctx.on("session:created", ({ sessionId }) => {
console.log("New session:", sessionId);
});
},
async destroy() {
// Routes and methods are cleaned up automatically on plugin removal
},
};
gateway.use(plugin);Plugin methods appear alongside built-in and config methods in the schema discovery response with builtin: false. Routes use longest-prefix matching — /v1 catches /v1/models, /v1/chat/completions, etc.
Built-In Protocol Plugins
The gateway ships three plugins that expose sessions via standard interfaces.
MCP Server — any MCP client (Claude Desktop, Cursor, Claude Code, etc.) can connect and use session tools and resources:
import { mcpServerPlugin } from "@agentick/gateway";
gateway.use(
mcpServerPlugin({
sessionId: "default", // omit for resources-only mode
path: "/mcp",
include: ["search", "read_file"],
exclude: ["dangerous_tool"],
// Static resources — domain knowledge, guides, etc.
resources: [
{
name: "guide",
uri: "myapp://guide/overview",
title: "Overview",
read: () => ({ text: "# Welcome\n\n..." }),
},
],
// Resource templates — parameterized, with listing and autocomplete
resourceTemplates: [
{
name: "schema",
uriTemplate: "myapp://schema/{model}",
title: "Model Schema",
list: () => [{ uri: "myapp://schema/users", title: "Users" }],
read: (vars) => ({ text: `# ${vars.model}\n\n...` }),
complete: { model: (v) => ["users", "orders"].filter((m) => m.startsWith(v)) },
},
],
}),
);Plugin configuration supports name, version, and description for the MCP server identity (sent in the initialize response). The older serverName and serverVersion fields still work but are deprecated:
gateway.use(
mcpServerPlugin({
name: "my-server", // replaces serverName (deprecated)
version: "2.0.0", // replaces serverVersion (deprecated)
description: "My MCP server for data tooling",
path: "/mcp",
// ...
}),
);Five modes:
- Pre-built server — pass a fully configured
MCPServerinstance viaserver; the plugin just bridges it to HTTP. The server'stoolFilterandtoolTransformcontrol per-session visibility usingMCPRequestContext(transport type, client info, user identity). - Resources-only — omit
sessionIdto serve MCP resources without tools - Standalone tools — register tools with their own handlers via
tools, no session required - Session tools — set
sessionIdto discover and expose agent session tools - Per-session tools — add
toolFilterto customize tools per client
Pre-built server — bring your own MCPServer from @agentick/mcp. The plugin skips all construction and just registers the HTTP route:
import { MCPServer } from "@agentick/mcp/server";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
tools: [
/* ... */
],
resources: [
/* ... */
],
apps: [
/* MCP Apps (ui:// resources) */
],
security: { authenticator: async () => ({ authenticated: true }) },
// Per-session tool visibility — filter based on transport, client, or user
toolFilter: (tool, ctx) => {
// Hide admin tools from non-admin users
if (tool.name.startsWith("admin_") && !ctx.user?.roles?.includes("admin")) return false;
// Hide recursive tools from in-process agents
if (ctx.session?.transportType === "in-process" && tool.name === "ask") return false;
return true;
},
});
gateway.use(mcpServerPlugin({ server, path: "/mcp" }));MCP Apps — serve interactive HTML micro-applications as ui:// resources. Apps render in the MCP client's sandboxed iframe and communicate back via the ext-apps bridge:
gateway.use(
mcpServerPlugin({
path: "/mcp",
apps: [
{
name: "dashboard",
uri: "ui://my-server/dashboard",
content: () => readFileSync("dist/dashboard.html", "utf-8"),
csp: { resourceDomains: ["esm.sh"] },
},
],
}),
);Standalone tools support MCP annotations (readOnlyHint, destructiveHint) via the annotations field, plus MCP Apps metadata via ui and _meta fields. When ui or _meta are set, the plugin forwards them to the underlying MCPServer so tools/list emits _meta.ui.resourceUri and spec-compliant hosts render the associated view:
gateway.use(
mcpServerPlugin({
path: "/mcp",
tools: [
{
name: "show_dashboard",
description: "Display an interactive dashboard",
inputSchema: { type: "object", properties: { projectId: { type: "string" } } },
ui: { resourceUri: "ui://my-server/dashboard" },
handler: async (args) => ({ content: [{ type: "text", text: "Dashboard rendered" }] }),
},
],
}),
);Tool calls dispatch through tool-dispatch.
For multi-user deployments, use toolFilter to customize tools per MCP client based on the incoming HTTP request:
gateway.use(
mcpServerPlugin({
sessionId: "default",
path: "/mcp",
toolFilter: async (tools, req) => {
const user = await authenticate(req);
return tools.filter((t) => user.allowedTools.includes(t.name));
},
}),
);Sessions are tracked by mcp-session-id header and cleaned up automatically.
When running behind Express or middleware that pre-parses request bodies, the plugin passes req.body to the transport automatically.
Logging — structured event logging for gateway lifecycle:
import { loggingPlugin } from "@agentick/gateway";
gateway.use(loggingPlugin());
// Logs: client connections, session create/close, plugin lifecycle, errorsOpenAI-Compatible — any OpenAI SDK client can send chat completions:
import { openaiCompatPlugin } from "@agentick/gateway";
gateway.use(
openaiCompatPlugin({
pathPrefix: "/v1",
modelMapping: { "gpt-4o": "coding", "gpt-4": "research" },
}),
);Serves POST /v1/chat/completions (streaming + non-streaming) and GET /v1/models. Model names route to gateway apps via modelMapping. Unmatched names fall back to the gateway's default app.
Configuration
The gateway has a built-in configuration system. Config is loaded from a JSON file, validated, and made available to all plugins and application code.
Config File
Create agentick.config.json in your project root:
{
"gateway": {
"port": 8080,
"host": "0.0.0.0"
},
"connectors": {
"telegram": {
"token": "${env:TELEGRAM_BOT_TOKEN}",
"allowedUsers": [12345678]
}
}
}Loading Config
import { loadConfig, bindConfig, createGateway } from "@agentick/gateway";
const configStore = await loadConfig({
path: "./agentick.config.json",
overrides: { gateway: { port: 9999 } }, // CLI flags, etc.
});
bindConfig(configStore);
const gateway = createGateway({
apps: { myApp },
defaultApp: "myApp",
configStore, // pass to gateway
});loadConfig returns a ConfigStore — it does not have side effects. Call bindConfig() yourself to make it globally available.
Reading Config
import { getConfig } from "@agentick/gateway";
const port = getConfig().get("gateway")?.port; // typed
const telegram = getConfig().get("connectors")?.telegram; // typed if augmentedExtending Config Types
Packages declare their config shape via module augmentation. This makes store.get() fully typed without the gateway knowing about every consumer:
// In your package:
declare module "@agentick/gateway" {
interface FileConfig {
myFeature?: {
enabled: boolean;
maxRetries?: number;
};
}
}Connector plugins augment ConnectorConfigs:
declare module "@agentick/gateway" {
interface ConnectorConfigs {
telegram?: {
token: string;
allowedUsers?: number[];
};
}
}Environment Variables and Secrets
String values matching ${env:VAR_NAME} resolve from process.env. String values matching ${secret:KEY} resolve from a SecretStore. Both are resolved before validation. Missing values throw ConfigValidationError.
{
"connectors": {
"telegram": {
"token": "${env:TELEGRAM_BOT_TOKEN}",
"apiKey": "${secret:MY_API_KEY}"
}
}
}Secret-interpolated values are tracked internally. The config RPC method returns a redacted version with secrets replaced by "***".
Schema Validation
Packages register schema fragments that validate their config section:
import { registerConfigSchema } from "@agentick/gateway";
registerConfigSchema("myFeature", {
parse: (data) => mySchema.parse(data),
_output: {} as MyFeatureConfig,
});Fragments are merged at startup. Keys without registered schemas pass through.
Plugin Access
Plugins receive config via PluginContext:
const plugin: GatewayPlugin = {
id: "my-plugin",
async initialize(ctx) {
const myConfig = ctx.config.get("myFeature");
},
};Protocol
The built-in config method returns the redacted configuration:
// Client call:
const response = await client.call("config");
// { config: { gateway: { port: 8080 }, connectors: { telegram: { token: "***" } } } }The config:changed event type is reserved for future hot-reload support.
With Express
import { createExpressMiddleware } from "@agentick/express";
const app = express();
app.use("/api", createExpressMiddleware({ gateway }));This mounts SSE endpoints for streaming and method endpoints for RPC.