Availability
| Edition | Deployment Type |
|---|---|
| Community & Enterprise | Self-Managed, Hybrid |
/plugins/{slug}/ URL namespace. This gives plugins full control over request handling, enabling use cases like OAuth identity providers, MCP proxy servers, webhook receivers, and custom protocol-specific APIs.
Overview
Custom Endpoints provide plugins with the ability to:- Serve custom HTTP APIs alongside the standard LLM/Tool/Datasource proxy endpoints
- Handle arbitrary URL paths with pre-split path segments for easy routing
- Stream responses via Server-Sent Events (SSE) for protocols like MCP Streamable HTTP
- Authenticate requests using the gateway’s existing token system, with full App context (including metadata)
- Support any HTTP method (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD)
Working Example
Seeexamples/plugins/gateway/custom-echo-endpoint/ for a complete, working example that combines CustomEndpointHandler + UIProvider + ConfigProvider. It echoes request metadata and serves content configurable via the Studio admin UI.
URL Pattern
All custom endpoints are mounted under:{slug} is declared in the plugin’s configuration (the slug key in the config map), and {path...} is the sub-path handled by the plugin.
How It Works
Implementing Custom Endpoints
Step 1: Implement CustomEndpointHandler Interface
Step 2: Register Your Endpoints
Declare which paths and HTTP methods your plugin handles:| Field | Type | Description |
|---|---|---|
path | string | Relative path under the plugin slug. Use /* for catch-all. |
methods | []string | HTTP methods to handle. Valid: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD. |
description | string | Human-readable description of the endpoint. |
require_auth | bool | If true, gateway validates the token and passes the full App object to the plugin. |
stream_response | bool | If true, gateway uses HandleEndpointRequestStream (SSE). Otherwise uses HandleEndpointRequest. |
metadata | map[string]string | Plugin-defined metadata. |
Step 3: Handle Requests
Unary (Non-Streaming) Endpoints
For standard request/response:Streaming (SSE) Endpoints
For Server-Sent Events or MCP Streamable HTTP:| Chunk Type | When | Fields Used |
|---|---|---|
HEADERS | First chunk | status_code, headers |
BODY | Zero or more times | data (raw bytes flushed to client) |
DONE | Final chunk | — |
ERROR | On failure | error_message |
Step 4: Configure the Plugin Slug and Register
The plugin slug (used in the URL path) must be set explicitly in the plugin’s config map. The slug determines the URL namespace:/plugins/{slug}/....
Manifest
Declarecustom_endpoint in the manifest’s capabilities:
"studio_ui" in hooks:
Registration via AI Studio
Register the plugin in AI Studio (Admin > Plugins) with:| Field | Value |
|---|---|
| Hook type | custom_endpoint |
| Hook types | ["custom_endpoint"] (add "studio_ui" if plugin has UI) |
| Config | Must include "slug" key — e.g., {"slug": "my-mcp"} |
slug in the config determines the URL path on the gateway. For example, {"slug": "my-mcp"} makes endpoints available at http://gateway:8081/plugins/my-mcp/....
Important: The slug must be set in the config map. Without it, endpoints will not be registered and you’ll see a warning in the gateway logs:
Building and Deploying
Gateway Loading
Custom endpoint plugins are loaded automatically at gateway startup via the pre-warming system. The gateway:- Queries all active plugins with
custom_endpointhook type - Loads each plugin (starts the binary, initializes via gRPC)
- Calls
GetEndpointRegistrations()to discover endpoints - Registers routes using the
slugfrom config
EndpointRequest Fields
When a request arrives, the plugin receives a richEndpointRequest:
| Field | Type | Description |
|---|---|---|
method | string | HTTP method (GET, POST, etc.) |
path | string | Full request path (/plugins/my-mcp/users/123) |
relative_path | string | Path relative to plugin mount (/users/123) |
path_segments | []string | Pre-split segments: ["users", "123"] |
headers | map[string]string | Request headers |
body | bytes | Request body |
query_string | string | Raw query string (foo=bar&baz=1) |
remote_addr | string | Client IP address |
host | string | Request Host header |
protocol | string | "http" (future: "websocket", "sse") |
context | PluginContext | Request ID, metadata |
authenticated | bool | Whether request was authenticated |
app | App | Full App object (when authenticated) |
scopes | []string | Token scopes (when authenticated) |
Path Segments
Thepath_segments field pre-splits the relative path for easy pattern matching:
| Request URL | relative_path | path_segments |
|---|---|---|
/plugins/my-mcp/ | / | [] |
/plugins/my-mcp/mcp | /mcp | ["mcp"] |
/plugins/my-mcp/users/123/profile | /users/123/profile | ["users", "123", "profile"] |
/plugins/my-mcp/.well-known/openid-configuration | /.well-known/openid-configuration | [".well-known", "openid-configuration"] |
Authentication and App Context
Whenrequire_auth: true, the gateway:
- Extracts the token from
Authorization: Bearer <token>header or?token=query param - Validates the token via the gateway’s auth provider
- Fetches the full App object linked to the token
- Populates
EndpointRequestwithauthenticated=true,app, andscopes
App object includes:
| Field | Type | Description |
|---|---|---|
id | uint32 | App ID |
name | string | App name |
description | string | App description |
owner_email | string | Owner email address |
is_active | bool | Whether app is active |
monthly_budget | double | Monthly budget limit |
rate_limit | int32 | Rate limit (requests per minute) |
metadata | map[string]string | Custom key-value metadata |
Access Control via App Metadata
The recommended pattern for per-app access control is to store ACL rules in App metadata, which admins configure per-app:Route Matching
The gateway matches routes in this order:- Exact match —
GET:/plugins/my-oauth/.well-known/openid-configuration - Wildcard catch-all —
GET:/plugins/my-oauth/*
/* catch-all and handle routing internally using path_segments. This is the simplest and most flexible approach.
A plugin can also register multiple specific paths:
MCP Streamable HTTP Support
Custom endpoints are designed to support MCP (Model Context Protocol) Streamable HTTP out of the box.MCP Protocol Summary
MCP Streamable HTTP uses a single endpoint with:- POST: Client sends JSON-RPC messages; server responds with
application/jsonortext/event-stream - GET: Client opens an inbound SSE stream for server-initiated messages
- DELETE: Client terminates the session
- Session tracking via
Mcp-Session-Idheader
MCP Proxy Plugin Pattern
Complete Example: Webhook Receiver
Lifecycle Management
Custom endpoint routes are managed automatically across all plugin lifecycle events:| Event | Route Behavior |
|---|---|
| Plugin loaded | Endpoints registered after Initialize() |
| Plugin unloaded | All routes for this plugin removed |
| Plugin reloaded | Routes cleared then re-registered |
| Plugin deactivated | Plugin unloaded, routes removed |
| Plugin deleted | Plugin unloaded, routes removed |
| Gateway shutdown | All routes cleared |
| Health check failure | Plugin auto-restarted, routes re-registered |
| Control plane sync | Routes refreshed to match new config |
Error Handling
| Scenario | HTTP Status |
|---|---|
| No route match | 404 Not Found |
| Plugin not loaded / unhealthy | 503 Service Unavailable |
| Plugin returns error via gRPC | 502 Bad Gateway |
| gRPC timeout (60s unary / configurable stream) | 504 Gateway Timeout |
| Auth required but missing/invalid | 401 Unauthorized |
| Streaming error after headers sent | Connection closed, error logged |
Configuration
Streaming Timeout
The streaming endpoint timeout is configurable via environment variable:| Environment Variable | Default | Description |
|---|---|---|
PLUGIN_ENDPOINT_STREAM_TIMEOUT | 5m | Maximum duration for streaming endpoint responses (SSE) |
PLUGIN_ENDPOINT_MAX_BODY_SIZE | 1048576 (1MB) | Maximum request body size for custom plugin endpoints |