Gateway#
Responsibilities#
- Expose MCP-compliant HTTP API
- Create tasks from HTTP requests
- Track task status in PostgreSQL
- Stream progress updates and ephemeral FLY events via Server-Sent Events (SSE)
- Receive status reports from crew actors
- Broadcast FLY events via PG LISTEN/NOTIFY for cross-process delivery
How It Works#
- Client calls MCP tool via HTTP POST
- Gateway creates task with unique ID
- Gateway stores task in PostgreSQL (status:
pending) - Gateway sends envelope to first actor's queue
- Crew actors (
x-sink,x-sump) report final task status - Client polls or streams task status updates via SSE
Deployment#
The gateway binary supports three modes via ASYA_GATEWAY_MODE:
| Mode | Routes | Use |
|---|---|---|
api |
A2A, MCP, OAuth, health | External-facing; behind Ingress |
mesh |
Mesh routes, health | Internal-only; ClusterIP, no Ingress |
testing |
All routes | Local development and integration tests |
State ownership#
The gateway uses two independent state stores with clearly separated ownership:
┌─────────────────────────────┐
│ gateway-flows ConfigMap │
│ (flows.yaml — K8s object) │
└──────────────┬──────────────┘
│ polls every 10 s
│ (toolstore.Watch)
┌──────────────▼──────────────┐
external client ───► │ api pod │
MCP / A2A / OAuth │ - MCP dispatch │
│ - A2A routing │
│ - agent card │
└──────────────┬──────────────┘
│ sends envelope
▼
actor queue
│
▼
actor pod
│ POST /mesh/{id}/…
┌──────────────▼──────────────┐
│ mesh pod │
│ - progress callbacks │
│ - final status │
│ - SSE fan-out │
└──────────────┬──────────────┘
│ reads / writes
┌──────────────▼──────────────┐
│ PostgreSQL │
│ tasks, task_updates │
│ oauth_clients, tokens │
└─────────────────────────────┘
▲
───────────────┘
api pod also reads/writes
(task creation, OAuth, GetTask)
ConfigMap (gateway-flows) — routing configuration:
| api pod | mesh pod | |
|---|---|---|
| Mounts ConfigMap | ✅ (ASYA_CONFIG_PATH) |
❌ |
| Hot-reloads on change | ✅ every 10 s (default) | ❌ |
| Uses for dispatch | ✅ MCP + A2A + agent card | ❌ |
The ConfigMap is the source of truth for what flows exist. It is seeded by
Helm at deploy time and can be patched at runtime (e.g., via kubectl patch or
asya mcp expose) without a pod restart.
Database — task and auth state (PostgreSQL):
| api pod | mesh pod | |
|---|---|---|
| Creates tasks | ✅ (on MCP/A2A call) | ❌ |
| Writes progress | ❌ | ✅ (via /mesh/{id}/progress) |
| Writes final status | ❌ | ✅ (via /mesh/{id}/final) |
| Reads task state | ✅ (GetTask, SSE, OAuth) | ✅ (SSE stream) |
| Stores OAuth clients/tokens | ✅ (when OAuth enabled) | ❌ |
Both pods connect to the same database instance. The database is the shared coordination point: the api pod creates a task record, then mesh pod workers update it as actors report progress.
The storage layer is abstracted internally. PostgreSQL is the provided implementation — it handles metadata and task state well for all current use cases. Additional backends may be added if specific requirements emerge. Note that this is distinct from the pluggable transports (SQS, RabbitMQ, Pub/Sub) and pluggable state proxy connectors (S3, GCS, Redis, NATS KV), which are designed for high-throughput data paths.
Production deployments use two Helm releases from the same chart:
# External-facing API gateway (with Ingress)
helm install asya-gateway deploy/helm-charts/asya-gateway/ \
--set mode=api \
-f gateway-values.yaml
# Internal mesh gateway (ClusterIP only, no Ingress)
helm install asya-gateway-mesh deploy/helm-charts/asya-gateway/ \
--set mode=mesh \
-f gateway-mesh-values.yaml
Both releases share the same container image and database. Sidecars
and crew actors reach the mesh deployment via in-cluster DNS:
asya-gateway-mesh.<namespace>.svc.cluster.local.
Gateway is stateful: Requires a database (PostgreSQL) for task tracking and (when OAuth is enabled) for OAuth client/token storage.
Configuration#
Configured via Helm values. Key sections:
# gateway-values.yaml
mode: api # api | mesh | testing
config:
sqsRegion: "us-east-1"
postgresHost: "postgres.default.svc.cluster.local"
postgresDatabase: "asya_gateway"
postgresPasswordSecretRef:
name: postgres-secret
key: password
Tool/skill registration is ConfigMap-backed: flows are declared in
flows.yaml, mounted into the api pod, and hot-reloaded every 10 seconds (configurable via ASYA_CONFIG_POLL_INTERVAL) by a
polling watcher (toolstore.Watch). Updating the ConfigMap is the only way to
add, change, or remove an exposed tool or A2A skill — no restart is needed.
The Helm chart creates an empty gateway-flows ConfigMap. After deployment,
flows are registered by patching the ConfigMap (e.g., via asya flow expose
or kubectl patch). No Helm upgrade is needed.
API Endpoints#
See: Gateway API spec for the full API reference with complete request/response schemas for all routes.
Routes are split across the two deployments.
External API routes (mode: api)#
MCP endpoints#
POST /mcp # MCP Streamable HTTP transport (recommended)
GET /mcp/sse # MCP SSE transport (for clients that require SSE)
POST /tools/call # MCP tool invocation (REST convenience path)
Both /mcp and /mcp/sse are active transports — neither is deprecated. Use
whichever your MCP client supports.
GET /.well-known/agent.json # A2A Agent Card (public, no auth)
POST /a2a/ # A2A JSON-RPC endpoint
OAuth 2.1 endpoints (when ASYA_MCP_OAUTH_ENABLED=true):
GET /.well-known/oauth-protected-resource # RFC 9728 resource metadata
GET /.well-known/oauth-authorization-server # RFC 8414 server metadata
POST /oauth/register # Dynamic Client Registration
GET /oauth/authorize # Authorization Code endpoint
POST /oauth/token # Token exchange and refresh
Mesh routes (mode: mesh)#
Called exclusively by sidecars and crew actors within the cluster.
Call Tool (REST, external api mode)#
POST /tools/call
Content-Type: application/json
{
"name": "text-processor",
"arguments": {
"text": "Hello world",
"model": "gpt-4"
}
}
Response (MCP CallToolResult):
{
"content": [
{
"type": "text",
"text": "{\"task_id\":\"5e6fdb2d...\",\"message\":\"Task created successfully\",\"status_url\":\"/mesh/5e6fdb2d...\",\"stream_url\":\"/mesh/5e6fdb2d.../stream\"}"
}
],
"isError": false
}
See Envelope Protocol for more details on task statuses.
Get Task Status#
GET /tasks/{id}
Response:
{
"id": "5e6fdb2d-1d6b-4e91-baef-73e825434e7b",
"status": "succeeded",
"message": "Task completed successfully",
"result": {"response": "Processed: Hello world"},
"progress_percent": 100,
"current_actor_idx": 2,
"current_actor_name": "postprocess",
"actors_completed": 3,
"total_actors": 3,
"created_at": "2025-11-18T12:00:00Z",
"updated_at": "2025-11-18T12:01:30Z"
}
Stream Task Updates (SSE)#
GET /mesh/{id}/stream
Accept: text/event-stream
Internal mesh endpoint for streaming progress and FLY events. For external API access, use GET /stream/{id} on the API gateway.
Features:
- Sends historical progress and status updates first (no missed progress)
- Streams real-time updates as they occur
- Broadcasts ephemeral FLY events via PG LISTEN/NOTIFY for cross-process delivery
- Keepalive comments every 15 seconds
- Auto-closes on final status (
succeededorfailed)
⚠️ FLY events are ephemeral — not persisted to the database. Clients connecting after task completion will NOT see historical FLY events.
Stream events (TaskUpdate):
event: update
data: {"id":"task-123","status":"running","progress_percent":10,"current_actor_idx":0,"actor_state":"received","actor":"preprocess","actors":["preprocess","infer","post"],"message":"Actor preprocess: received","timestamp":"2025-11-18T12:00:15Z"}
event: update
data: {"id":"task-123","status":"running","progress_percent":33,"current_actor_idx":0,"actor_state":"completed","actor":"preprocess","actors":["preprocess","infer","post"],"message":"Actor preprocess: completed","timestamp":"2025-11-18T12:00:20Z"}
event: update
data: {"id":"task-123","status":"running","progress_percent":66,"current_actor_idx":1,"actor_state":"completed","actor":"infer","actors":["preprocess","infer","post"],"message":"Actor infer: completed","timestamp":"2025-11-18T12:01:00Z"}
event: update
data: {"id":"task-123","status":"succeeded","progress_percent":100,"result":{...},"message":"Task completed successfully","timestamp":"2025-11-18T12:01:30Z"}
TaskUpdate fields:
id: Task IDstatus: Task status (pending,running,succeeded,failed)progress_percent: Progress 0-100 (omitted if not a progress update)current_actor_idx: Current actor index (0-based, omitted for final states)actor_state: Actor processing state (received,processing,completed)actor: Current actor name (omitted for final states)actors: Full route (may be modified via VFS)message: Human-readable status messageresult: Final result (only forsucceededstatus)error: Error message (only forfailedstatus)timestamp: When this update occurred
Check Task Active#
GET /mesh/{id}/active
Used by: Actors to verify task hasn't timed out
Response (active):
{"active": true}
Response (inactive - HTTP 410 Gone):
{"active": false}
Mesh endpoints (Sidecar/Crew, mode: mesh)#
Report Progress#
POST /mesh/{id}/progress
Content-Type: application/json
{
"actors": ["prep", "infer", "post"],
"current_actor_idx": 0,
"status": "completed"
}
Called by: Sidecars at three points per actor (received, processing, completed)
Progress formula: (actor_idx * 100 + status_weight) / total_actors
- received = 10, processing = 50, completed = 100
Unknown task IDs: Progress updates for tasks not found in the store are silently accepted (200 OK). This is expected for direct-SQS envelopes that bypass gateway task creation. Infrastructure errors (e.g., database failures) still return 500.
Response:
{"status": "ok", "progress_percent": 33.3}
Report Final Status#
POST /mesh/{id}/final
Content-Type: application/json
{
"id": "task-123",
"status": "succeeded",
"result": {...}
}
Called by: x-sink (success) or x-sump (failure) crew actors
Create Fanout Task#
POST /tasks
Content-Type: application/json
{
"id": "task-123-1",
"parent_id": "task-123",
"actors": ["prep", "infer"],
"current": 1
}
Called by: Sidecars when runtime returns array (fan-out)
Fanout ID semantics:
- Index 0: Original ID (
task-123) - Index 1+: Suffixed (
task-123-1,task-123-2) - All children have
parent_idfor traceability
Health Check#
GET /health
Response: OK
Flow Examples#
Flows are declared in flows.yaml (mounted as a ConfigMap). Each entry is a
FlowConfig that maps a name to an actor pipeline and declares whether it is
exposed as an MCP tool, an A2A skill, or both.
Single-actor MCP tool:
flows:
- name: hello
entrypoint: hello-actor
description: Say hello
mcp:
inputSchema:
type: object
properties:
who:
type: string
description: Name to greet
required: [who]
Multi-actor pipeline exposed as both MCP and A2A:
flows:
- name: image-enhance
entrypoint: download-image
route_next: [enhance, upload]
description: Enhance image quality
timeout: 120
mcp:
inputSchema:
type: object
properties:
image_url:
type: string
description: URL of image to enhance
quality:
type: string
description: "Target quality: low | medium | high"
required: [image_url]
a2a: {}
Fields:
| Field | Required | Description |
|---|---|---|
name |
✅ | Unique flow name; becomes the MCP tool name and A2A skill name |
entrypoint |
✅ | First actor in the pipeline (queue name without asya-<ns>- prefix) |
route_next |
❌ | Ordered list of subsequent actors |
description |
❌ | Human-readable description surfaced in tool/skill listings |
timeout |
❌ | Max seconds to wait for completion (default: no limit) |
mcp |
❌ | Present → exposed as MCP tool; inputSchema is the JSON Schema for arguments |
a2a |
❌ | Present → exposed as A2A skill |
Authentication & Security#
The gateway implements protocol-native authentication on external routes and network-level isolation for mesh routes. No auth code runs on mesh routes — they are unreachable from outside the cluster by design.
| Route group | Auth mechanism |
|---|---|
A2A (/a2a/) |
API key (X-API-Key) or JWT Bearer — configured via ASYA_A2A_* env vars |
MCP (/mcp, /mcp/sse, /tools/call) |
API key Bearer or OAuth 2.1 token — configured via ASYA_MCP_* env vars |
Mesh (/mesh/…) |
None — ClusterIP only, unreachable externally |
| Well-known + health | Always public |
A2A Authentication#
Two schemes are supported with OR semantics — a request is authenticated if either check passes:
API Key
X-API-Key: <value>
Configured via ASYA_A2A_API_KEY. When set, the header value must match
exactly (constant-time comparison).
JWT Bearer
Authorization: Bearer <JWT>
Configured via ASYA_A2A_JWT_JWKS_URL + ASYA_A2A_JWT_ISSUER +
ASYA_A2A_JWT_AUDIENCE. The gateway fetches the JWKS from the configured URL
and validates the token signature, issuer, and audience claims.
When neither ASYA_A2A_API_KEY nor ASYA_A2A_JWT_JWKS_URL is set, A2A auth
is disabled (all requests pass). This is the default for local development.
The public Agent Card at /.well-known/agent.json advertises the configured
schemes. Authenticated clients access only their own tasks — the gateway must
not reveal the existence of tasks belonging to other clients.
MCP Authentication#
MCP auth is applied to /mcp, /mcp/sse, and /tools/call. Two modes are
mutually exclusive:
API Key (simple, non-spec-compliant)
Authorization: Bearer <static-key>
Set ASYA_MCP_API_KEY to a shared secret. Suitable for internal tooling
(asya-lab CLI, known MCP hosts) where full OAuth is not needed.
When ASYA_MCP_API_KEY is empty, MCP auth is disabled.
OAuth 2.1 (full MCP spec compliance)
Set ASYA_MCP_OAUTH_ENABLED=true plus ASYA_MCP_OAUTH_ISSUER and
ASYA_MCP_OAUTH_SECRET. The gateway acts as its own authorization server,
issuing HMAC-SHA256 JWTs. PostgreSQL is required (ASYA_DATABASE_URL).
Scopes (issued but not yet enforced per-endpoint):
| Scope | Intended permission |
|---|---|
mcp:invoke |
Call tools, send messages |
mcp:read |
List tools, read task state |
Scopes are issued into access tokens and stored in the database. However,
MCPAuthMiddleware currently only validates that a token is authentic (signature,
iss, aud, exp) — it does not check that the token's scope is sufficient
for the specific operation being requested.
OAuth 2.1 endpoints (when ASYA_MCP_OAUTH_ENABLED=true):
GET /.well-known/oauth-protected-resource # RFC 9728 resource metadata
GET /.well-known/oauth-authorization-server # RFC 8414 server metadata
POST /oauth/register # Dynamic Client Registration
GET /oauth/authorize # Authorization Code endpoint
POST /oauth/token # Token exchange and refresh
PKCE (code_challenge_method=S256) is required for all clients.
Dynamic Client Registration (/oauth/register) is public by default. To restrict
it, set ASYA_MCP_OAUTH_REGISTRATION_TOKEN — callers must then supply
Authorization: Bearer <registration-token> to register.
Mesh Security#
Mesh routes carry no authentication code. Security is enforced at the network layer:
asya-gateway-meshK8s Service isClusterIP— no Ingress, no NodePort. It is physically unreachable from outside the cluster.- Sidecars and crew actors reach it via in-cluster DNS:
asya-gateway-mesh.<namespace>.svc.cluster.local.
For defence in depth, add a K8s NetworkPolicy restricting ingress to actor pods:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: gateway-mesh-ingress
spec:
podSelector:
matchLabels:
app: asya-gateway-mesh
ingress:
- from:
- podSelector:
matchLabels:
asya.sh/component: actor
ports:
- port: 8080
Alternatively, enable a service mesh (Istio/Linkerd) for automatic mTLS between all pods with zero Asya code changes.
Environment Variables#
All auth-related environment variables:
| Variable | Default | Required | Description |
|---|---|---|---|
ASYA_GATEWAY_MODE |
— | Yes | api, mesh, or testing |
ASYA_DATABASE_URL |
"" |
For OAuth 2.1 | PostgreSQL DSN; required when ASYA_MCP_OAUTH_ENABLED=true |
| A2A | |||
ASYA_A2A_API_KEY |
"" |
No | Static API key; auth disabled when empty |
ASYA_A2A_JWT_JWKS_URL |
"" |
No | JWKS endpoint URL for JWT validation |
ASYA_A2A_JWT_ISSUER |
"" |
With JWKS | Expected iss claim |
ASYA_A2A_JWT_AUDIENCE |
"" |
With JWKS | Expected aud claim |
| MCP Phase 2 | |||
ASYA_MCP_API_KEY |
"" |
No | Static Bearer token; auth disabled when empty |
| MCP Phase 3 (OAuth 2.1) | |||
ASYA_MCP_OAUTH_ENABLED |
false |
No | Set to true to enable OAuth 2.1 |
ASYA_MCP_OAUTH_ISSUER |
"" |
Yes (OAuth) | Issuer URL embedded in tokens and metadata |
ASYA_MCP_OAUTH_SECRET |
"" |
Yes (OAuth) | HMAC-SHA256 signing key for access tokens |
ASYA_MCP_OAUTH_TOKEN_TTL |
3600 |
No | Access token lifetime in seconds |
ASYA_MCP_OAUTH_REGISTRATION_TOKEN |
"" |
No | Bearer token protecting /oauth/register; empty = open |
Using MCP tools#
See: Quickstart for instructions how to test MCP locally.
Deployment Helm Charts#
See: Gateway Setup Guide for gateway chart details.