mcpField
I
indykite-agent-gateway

indykite-agent-gateway

github.com/indykiteSecurity466 installsUpdated 3d ago
Repository

About indykite-agent-gateway

A skill for configuring IndyKite Agent Gateway to enforce identity, workflow authorization, and delegation checks before requests reach an A2A agent or MCP server. It covers single- and multi-hop delegation, configuration, and deployment.

Key features

  1. 01Protect one A2A agent or MCP server
  2. 02Validate caller identity and workflow access
  3. 03Check modeled delegation chains
  4. 04Configure IdP or Token Service exchanges
  5. 05Deploy and diagnose gateway instances

Use cases

  1. 01Enforcing authorization before an MCP server
  2. 02Protecting agents in an A2A workflow
  3. 03Setting up multi-hop delegation
  4. 04Investigating gateway authorization failures

MCP support

In `mcp` mode, IAG proxies MCP Streamable HTTP traffic to a protected MCP server and applies the same identity, authorization, and delegation checks used for A2A. It forwards only the specified MCP protocol revisions.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/indykite/skills --skill indykite-agent-gateway

After installing, type/indykite-agent-gatewayin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

assets/
config-template.yaml
token-service-config-template.yaml
references/
architecture.md
configuration.md
token-service.md
troubleshooting.md

Details

From the project README

IndyKite Agent Gateway

The Indykite Agent Gateway (IAG) is a standalone service that protects exactly one downstream - an A2A agent or an MCP server. Run one IAG per protected downstream. From the caller's perspective IAG appears as the Target; from the protected downstream's perspective IAG appears as the Source. IAG is not a generic reverse proxy: by default (protocol: a2a) it speaks the A2A protocol and tracks A2A sessions so JSON-RPC streams flow correctly; with protocol: mcp it proxies MCP Streamable HTTP traffic (forwarding Mcp-Session-Id and streaming SSE responses through) and forwards only the MCP protocol revisions 2025-06-18, 2025-11-25, and 2026-07-28. Either way the same authorization runs in front.

For each request IAG validates three things:

  1. The caller (token introspection at the IdP).
  2. The workflow (subject CAN_TRIGGER check via AuthZEN/KBAC). Since gateway 2.82.0 the subject comes from ContX IQ GET /contx-iq/v1/whoami on the caller's token: its type is the ikg_node_type of the project's Token Introspect configuration that validates the token, its id the token's subject (sub, or the claim named by that configuration's sub_claim).
  3. The delegation chain (the request's act chain matches a chain modeled in the IKG).

The delegation token that carries the act chain is minted either by the IdP (RFC 8693 token exchange) or, when the optional token_service section is configured, by the self-hosted IndyKite Token Service (ITS). With ITS the caller's own access token travels untouched in Authorization and the delegation token beside it in X-IK-Token, and a request that arrives with an X-IK-Token is a later hop whose chain is nested into the next token - which is what makes workflows longer than one hop work.

When to use

Activate this skill when the user:

  • is building an agent-to-agent (A2A) workflow and wants policy enforcement in front of each agent;
  • is putting an enforcement point in front of an MCP server (protocol: mcp) so MCP traffic gets the same introspection, AuthZEN check, and audit as A2A;
  • needs traceable user-to-agent delegation through OAuth token exchange and the act chain, including multi-hop chains that require the IndyKite Token Service and the X-IK-Token header;
  • is configuring the Token Service itself (issuer base_url, signing keys, audiences, token_introspection configurations);
  • is modeling a Workflow and Agent nodes with INVOKES relationships in the IndyKite Graph (IKG);
  • is configuring JARVIS_* environment variables or a config.yaml for one or more IAG instances (including token_service, healthcheck_port, cache tuning, audit delivery);
  • is reading IAG or Token Service audit records (AUTHORIZED / NOT_AUTHORIZED, TOKEN_EXCHANGED / EXCHANGE_REFUSED, INTROSPECTED*) and trying to explain a 401 or 403;
  • is reproducing or extending the iag-mcp-demo (A2A + MCP), or iag-token-exchange (Token Service) reference applications.

Do not activate this skill when the user:

  • wants a generic HTTP reverse proxy or service mesh - IAG only speaks A2A or MCP, not arbitrary HTTP;
  • is asking about IndyKite features unrelated to agent gating (Token Introspect configurations beyond the ones the gateway's callers need, plain AuthZEN policies, ContX IQ queries outside of agent gating);
  • is debugging the protected agent itself rather than the gateway in front of it.

Prerequisites

Before any of the steps below will succeed, the user needs:

  • An IndyKite project with AuthZEN / KBAC and ContX IQ enabled, plus a Workflow node and Agent nodes already modeled (or a plan to model them - see Step 1).
  • An OAuth2-compliant IdP with introspect, client-credentials, and token-exchange endpoints. Set IDP_BASE_URL to the IdP base for the target environment. The IdP stays required even with the Token Service: it introspects the caller's access token and authenticates the protected agent.
  • One client_id / client_secret pair per protected downstream (A2A agent or MCP server), registered with the IdP.
  • A ContX IQ query that returns (workflow, agent_list) pairs for each protected downstream - its query_id goes into IAG configuration.
  • For an MCP downstream (protocol: mcp): the MCP server's origin and endpoint path, and the gateway image indykite/agent-gateway ≥ 2.0.1 (older images ignore the protocol and behave as an A2A proxy). MCP clients must be introspectable callers covered by a Token Introspect configuration (gateway 2.82.0+, next bullet) and pass the AuthZEN check.
  • Since gateway 2.82.0, a Token Introspect configuration for every caller of a gateway; its ikg_node_type is the subject type the trigger policy must name - see the subject step (before 2.82.0, authzen.subject_types).
  • For workflows longer than one hop (agent → agent → agent), or whenever the protected agent must see the caller's original access token: a running IndyKite Token Service (indykite/token-service) with an https issuer base_url, at least one signing key, the audiences of every protected agent, and a client_auth client for the gateways - see references/token-service.md. Without it the gateway exchanges tokens at the IdP on every hop and the chain never grows past one hop.
  • Docker (and Docker Compose) to run the iag-mcp-demo reference app.

If any of these are missing, stop and tell the user - IAG cannot run without them.

Steps

1. Model the workflow in the IKG

Create one Workflow node identified by external_id, one Agent node per protected agent, and INVOKES relationships between agents. Every INVOKES relationship must carry a workflow_name property whose value matches the Workflow.external_id. The ContX IQ query in step 3 silently excludes any chain without it.

Example shape (the canonical wf1 workflow from the demo):

(Workflow {external_id: "wf1"})
(Agent {external_id: "orchestrator"})
  -[INVOKES {workflow_name: "wf1"}]-> (Agent {external_id: "retriever"})
(Agent {external_id: "orchestrator"})
  -[INVOKES {workflow_name: "wf1"}]-> (Agent {external_id: "weather"})

Capture this data through the Capture API (POST /capture/v1/nodes, POST /capture/v1/relationships), the IndyKite Hub UI, or an existing Terraform / identity pipeline - whichever the project already uses.

2. Wire the subject to the workflow

For every subject that is allowed to trigger the workflow, create the edge (:User)-[:CAN_TRIGGER]->(:Workflow) (or the equivalent AuthZEN relation). Without this edge IAG returns 403 at the AuthZEN check even if the chain is correct.

3. Build the ContX IQ query

The query must return (workflow, agent_list) pairs given a protected agent identifier, where agent_list is the ordered chain of agents the request must traverse. Save its query_id - IAG references it via JARVIS_CONTX_IQ_QUERY_ID (or contx_iq.query_id).

If the KBAC policy behind CAN_TRIGGER reads the delegation token, it must reference the token the gateway actually forwards: $token when the IdP mints it (it replaces Authorization), $ik_token when the Token Service mints it (it travels in X-IK-Token). A reference to a token that was not sent resolves to nothing, and the decision is a denial.

4. Configure each IAG instance

Pick one of the two configuration forms:

  • YAML config file - pass with --config=/app/config.yaml. Best for production where configuration management is strict. See assets/config-template.yaml in this skill.
  • Environment variables - keys use the JARVIS_ prefix with underscores (e.g. JARVIS_SERVICE_NAME). Best when multiple IAG instances share a base image (as in the iag-mcp-demo) and override only per-agent fields.

The full set of sections is service, identity_provider, token_service (optional), protected_agent, authzen, contx_iq, and audit. See references/configuration.md for every field and its default.

Pick the downstream protocol with protected_agent.protocol (env JARVIS_PROTECTED_AGENT_PROTOCOL): a2a (default) for an A2A agent, mcp for an MCP server. Any other value fails startup with invalid protected_agent protocol. In mcp mode protected_agent.base_url is the MCP server origin only - IAG appends the incoming request path. The authorization sequence is unchanged; only the forwarded protocol differs. In mcp mode the gateway also enforces the MCP protocol revision: only 2025-06-18, 2025-11-25, and 2026-07-28 are forwarded, every request other than initialize must carry Mcp-Protocol-Version with one of them, and JSON-RPC batches, encoded bodies, and bodies over 4 MiB are refused with a JSON-RPC error before authorization. The MCP server behind the gateway must speak at least one of the three revisions. See the Protecting an MCP server section of references/configuration.md and the MCP protocol revisions section of references/architecture.md.

Pick where the delegation token comes from with the optional token_service section (base_url, exchange_endpoint, introspect_endpoint, client_auth.type: client_secret_basic, client_auth.client_id, client_auth.client_secret; env JARVIS_TOKEN_SERVICE_*). Left out, the IdP exchanges the tokens. Filled in, the Token Service mints the delegation token, the request is forwarded with both Authorization and X-IK-Token, and an incoming X-IK-Token is validated at the Token Service and becomes the subject of the next exchange. When set, also list the audiences the gateway requests in protected_agent.authentication.audiences - every one of them must be in the Token Service's idp.audiences, or the exchange is refused with invalid_target.

Per-instance values that must differ between IAG instances:

  • service.name / JARVIS_SERVICE_NAME
  • service.port / JARVIS_SERVICE_PORT
  • service.healthcheck_port / JARVIS_SERVICE_HEALTHCHECK_PORT (only when several instances share one network namespace; default 9080)
  • protected_agent.base_url / JARVIS_PROTECTED_AGENT_BASE_URL
  • protected_agent.protocol / JARVIS_PROTECTED_AGENT_PROTOCOL (if any instance protects an MCP server)
  • protected_agent.authentication.client_id and client_secret
  • protected_agent.authentication.audiences (with the Token Service, the audience of that instance's protected agent)

The token_service section itself is the same for every instance.

5. Deploy the IAG instances (and the Token Service, if used)

For Docker Compose, follow the iag-mcp-demo pattern: one shared iag-base-docker.yaml plus one service per protected agent that overrides only the per-instance fields above. For Kubernetes, deploy each IAG as a standalone Pod for independent scaling, or as a sidecar to its protected agent for tighter coupling.

Each gateway serves /healthz, /readyz, and /startupz on service.healthcheck_port (default 9080), and the container image's HEALTHCHECK probes http://localhost:9080/healthz. Older images did not serve that endpoint and reported unhealthy for their whole life - pull a current image if orchestration keyed on container health never fires.

The Token Service is one deployment shared by all gateways: run indykite/token-service (pin a concrete tag such as 1.0.0, never latest) with --config=<file> or, in production, --config-dir=<dir> so the secret (signing keys, client_auth) can be mounted as a separate file merged over the non-sensitive definitions. service.base_url (the iss of every token it signs) is mandatory, has no default, and must be https. Details in references/token-service.md.

The IndyKite platform must also trust the tokens ITS signs, or every request that reaches AuthZEN, ContX IQ, or the MCP server with an X-IK-Token is refused with 401 Invalid token in X-IK-Token header. Create one Token Introspect configuration per audience the delegation token can carry (POST /configs/v1/token-introspects, Service Account token): jwt_matcher.issuer = the ITS base_url, jwt_matcher.audience = the protected agent's audience, offline_validation.public_jwks = the public half of the signing key. Exact payload in the Making the platform trust ITS tokens section of references/token-service.md.

Verify that each IAG can reach the IdP, AuthZEN, ContX IQ, the Token Service (if configured), and the protected agent - common deployment failures are network-level, not IAG-level.

6. Verify the runtime path

Send a request through the gateway and confirm the nine-step path executes end-to-end. The expected HTTP responses are:

  • 200 - request was authorized and forwarded.
  • 400 - bad request. In mcp mode a JSON-RPC error body: -32020 (Mcp-Protocol-Version header missing, or on 2026-07-28 an Mcp-Method / Mcp-Name header that differs from the body), -32022 (unsupported revision, error.data.supported lists the three revisions), -32600 (JSON-RPC batch, not a JSON-RPC 2.0 message, an ambiguous key, or a method / tool name / URI with control characters), -32602 (a tools/call that names no tool, or a resources/read, resources/subscribe or resources/unsubscribe that names no resource URI), or -32700 (body could not be read, is not valid UTF-8, or is not exactly one JSON value). The gateway reads the tool or resource every request names and refuses what it cannot read unambiguously - MCP request classification.
  • 401 - caller token missing or inactive (Missing bearer token, Invalid token, missing subject), since 2.82.0 a token no Token Introspect configuration of the project accepts ({"message":"Unauthorized","errors":["ContX IQ whoami failed"]}, audit reason cannot resolve the subject of the access token), or an incoming X-IK-Token that cannot be used (Invalid delegated token, subject mismatch / missing subject).
  • 403 - caller authenticated but not allowed (subject, chain, or both) - Authorization check failed. The reason is only in the audit record: no workflow matches the actors chain (a skipped hop, or a delegation token reused on another chain) or subject <type>/<id> is not authorized to trigger any of the workflows <ids> (the chain matches, the user holds no CAN_TRIGGER; before 2.82.0 without <type>/<id>) - chain refusals.
  • 413 / 415 - mcp mode only: body over 4 MiB, or a Content-Encoding other than identity; JSON-RPC error -32600.
  • 500 - internal error.
  • 502 - upstream / gateway-side processing error, including an IdP or Token Service that cannot be reached or answers unreadably. A provider outage is reported as a fault, never counted as a denial.

Watch service logs (JSON to stdout) and audit records together - service logs explain what IAG did, audit records explain what IAG decided. A refused IdP or Token Service call is logged with the endpoint, the status, and a sanitized preview of the response body, so a wrong client secret and a broken endpoint no longer look the same.

7. Read the audit trail

Audit records are emitted as a separate stream from service logs. Configure delivery as either:

  • Webhook - audit.delivery: webhook, audit.http.url, audit.http.method (post or put), auth type one of no-auth, api-key, mTLS.
  • File - audit.delivery: file, audit.file.storage_path, audit.file.format (csv, json, txt), audit.file.rotation_strategy (size, time, size-and-time).

Either form can be buffered with audit.async.buffer_size (default 512); buffered records are drained on shutdown, and a full queue drops records with a warning in the service log.

Each gateway record contains decision (AUTHORIZED, NOT_AUTHORIZED, or ERROR when the request could not be decided), reason, subject, actor, actorsChain, action, service, timestamp, traceID. The traceID correlates with the protected agent's logs and the upstream client. The Token Service writes records of the same shape for every exchange and introspection, with the decisions TOKEN_EXCHANGED / EXCHANGE_REFUSED, INTROSPECTED / INTROSPECTED_INACTIVE / INTROSPECTION_REFUSED, and ERROR, plus tokenID (the token's jti).

8. Exercise denial paths intentionally

To gain confidence that IAG is enforcing as expected, deliberately force NOT_AUTHORIZED outcomes:

  • Skip an agent in the chain (e.g. call retriever-iag directly without the orchestrator in act).
  • Remove workflow_name from one INVOKES relationship - ContX IQ stops returning that chain.
  • Delete the CAN_TRIGGER edge between the subject and the workflow - AuthZEN says no.
  • Call with a token whose Token Introspect configuration has an ikg_node_type no trigger policy names - no policy matches, 403 (before 2.82.0: a subject type missing from JARVIS_AUTHZEN_SUBJECT_TYPES).
  • With the Token Service: replay an X-IK-Token with a different user's access token - 401 Invalid delegated token, subject mismatch.
  • On an mcp instance: send tools/list without the Mcp-Protocol-Version header - 400 with JSON-RPC error -32020, audited as NOT_AUTHORIZED with reason MCP request refused: Header mismatch: Mcp-Protocol-Version header is missing.
  • On an mcp instance: send a tools/call whose params has no name - 400 with JSON-RPC error -32602, audited as NOT_AUTHORIZED with reason MCP request refused: Invalid params: tools/call names no tool; the server never sees it.

Each should return 403 Forbidden (or 401 for the token pairing, 400 for the MCP revision and classification checks) and produce a NOT_AUTHORIZED audit record with a useful reason.

Outcome

When this skill has been applied successfully:

  • A Workflow node, the relevant Agent nodes, and INVOKES edges (with workflow_name) exist in the IKG.
  • A ContX IQ query returns the right (workflow, agent_list) pairs for each protected agent.
  • One IAG instance runs in front of each protected agent with the right per-instance config, reporting healthy on /healthz.
  • On gateway 2.82.0+, the indykite-ciq-whoami skill resolves every caller of a gateway to the subject type the trigger policy names.
  • If the workflow has more than one hop, the Token Service runs, every gateway points at it, the project holds a Token Introspect configuration for each audience ITS issues for, and the forwarded request carries Authorization plus X-IK-Token with a chain that grows on every hop.
  • A canonical successful prompt flows through the gateway chain and produces AUTHORIZED audit records on every IAG it touches (and TOKEN_EXCHANGED records at the Token Service).
  • A canonical denial path produces 403 Forbidden plus a NOT_AUTHORIZED audit record with a human-readable reason.

Files in this skill

Agent-specific notes

This skill uses generic markdown instructions and works across all agents listed in the README. It does not require Claude Code hooks, Cursor @-mentions, Copilot workspace context, or any agent-specific feature. Network access (curl, the agent's web tools, or MCP) is needed only if the agent will call IndyKite APIs directly during a task; for setup-only work no special tools are required.

References

Comments

Markdown supported