Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion a2a/iag-demo/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ CIQ_QUERY_STORE_DECISION=store-decision
CIQ_QUERY_HQ_WEATHER=get-hq-weather

# [Required] MCP configuration
IK_APP_AGENT_KEY=
# NOTE: the MCP server resolves the AppAgent identity server-side from the
# project's MCP server configuration (app_agent_id). Callers send only the user's
# Bearer token; the previously-required IK_APP_AGENT_KEY / X-IK-ClientKey is no
# longer used.
MCP_SERVER_URL=https://dev.mcp.indykite.com/mcp/v1/<PROJECT_GID_URL_ENCODED>

# [Required] From your IdP Provider configuration
Expand Down
2 changes: 1 addition & 1 deletion a2a/iag-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Fill in, at a minimum:
| `INDYKITE_BASE_URL` | `https://api.eu.indykite.com` or `https://api.us.indykite.com` |
| `CIQ_QUERY_ID` | Knowledge query ID or name from your project |
| `WORKFLOW_ID` | The `external_id` of the single `Workflow` node to whitelist (sets `JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID` in [`iag-base-docker.yaml`](iag-base-docker.yaml)). If unset/removed, all workflows defined in the IKG are considered when authorizing requests. |
| `APP_AGENT_CREDENTIALS_TOKEN` / `IK_APP_AGENT_KEY` | App Agent credentials token |
| `APP_AGENT_CREDENTIALS_TOKEN` | App Agent credentials token used by the gateway for its ContX IQ calls (`JARVIS_CONTX_IQ_APP_AGENT_CREDENTIALS_TOKEN`) |
| `MCP_SERVER_URL` | `https://us.mcp.indykite.com/mcp/v1/<PROJECT_GID_URL_ENCODED>` `https://eu.mcp.indykite.com/mcp/v1/<PROJECT_GID_URL_ENCODED>` |
| `CHATBOT_IDP_CLIENT_ID` / `_SECRET` | IdP Provider `console` client |
| `ORCHESTRATOR_IDP_CLIENT_ID` / `_SECRET` | IdP Provider `indykiteagent` client |
Expand Down
2 changes: 0 additions & 2 deletions a2a/iag-demo/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ services:
RETRIEVER_PORT: ${RETRIEVER_PORT}
RETRIEVER_AGENT_NAME: retriever_agent
MCP_SERVER_URL: ${MCP_SERVER_URL}
IK_APP_AGENT_KEY: ${IK_APP_AGENT_KEY}
GEMINI_ENABLED: ${GEMINI_ENABLED}
GEMINI_API_KEY: ${GEMINI_API_KEY}
GEMINI_MODEL: ${GEMINI_MODEL}
Expand Down Expand Up @@ -172,7 +171,6 @@ services:
WEATHER_PORT: ${WEATHER_PORT}
WEATHER_AGENT_NAME: weather_agent
MCP_SERVER_URL: ${MCP_SERVER_URL}
IK_APP_AGENT_KEY: ${IK_APP_AGENT_KEY}
INDYKITE_BASE_URL: ${INDYKITE_BASE_URL}
CIQ_QUERY_HQ_WEATHER: ${CIQ_QUERY_HQ_WEATHER:-get-hq-weather}
LOG_LEVEL: ${LOG_LEVEL}
Expand Down
1 change: 0 additions & 1 deletion a2a/iag-demo/retriever_agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ RETRIEVER_PORT=6002
RETRIEVER_AGENT_NAME=retriever_agent
LLM_MODEL=qwen3:14b-q8_0
MCP_SERVER_URL=https://us.mcp.indykite.com/mcp/v1/gid%3AAAAAApo7TvCs_0TQt-WFbP5KxMM
IK_APP_AGENT_KEY=
GEMINI_API_KEY=
GEMINI_MODEL=gemini-2.5-pro
# Use Gemini instead of local Ollama when true (GEMINI_ENABLED or GEMENI_ENABLED)
Expand Down
6 changes: 3 additions & 3 deletions a2a/iag-demo/retriever_agent/retriever_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ async def _patched_handle_post_request(self, ctx):
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "").strip()
MCP_AUTH_HEADER = os.getenv("IK_APP_AGENT_KEY", "").strip()
INDYKITE_BASE_URL = os.getenv("INDYKITE_BASE_URL", "").strip()
CIQ_QUERY_STOCK_PRICE = os.getenv("CIQ_QUERY_STOCK_PRICE", "").strip() or "get-stock-quote"
CIQ_QUERY_PURCHASE_LIMIT = os.getenv("CIQ_QUERY_PURCHASE_LIMIT", "").strip() or "get-stock-trade-threshold"
Expand Down Expand Up @@ -846,9 +845,10 @@ async def _mcp_session(access_token: str = ""):
yield []
return

# The MCP server resolves the AppAgent identity server-side from the project's
# MCP server configuration (app_agent_id); the caller sends only the user's
# Bearer token. X-IK-ClientKey is no longer used.
headers: dict[str, str] = {}
if MCP_AUTH_HEADER:
headers["X-IK-ClientKey"] = MCP_AUTH_HEADER
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
if INDYKITE_BASE_URL:
Expand Down
1 change: 0 additions & 1 deletion a2a/iag-demo/weather_agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ LOG_LEVEL=INFO
# ciq_execute against the canbank get-hq-weather knowledge query instead of
# hitting Open-Meteo directly. Other cities always take the direct path.
MCP_SERVER_URL=
IK_APP_AGENT_KEY=
INDYKITE_BASE_URL=
CIQ_QUERY_HQ_WEATHER=get-hq-weather
6 changes: 3 additions & 3 deletions a2a/iag-demo/weather_agent/weather_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ async def _patched_handle_post_request(self, ctx):
DEFAULT_CITY = os.getenv("WEATHER_DEFAULT_CITY", "London").strip()
WEATHER_TIMEOUT = float(os.getenv("WEATHER_TIMEOUT", "15"))
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "").strip()
MCP_AUTH_HEADER = os.getenv("IK_APP_AGENT_KEY", "").strip()
INDYKITE_BASE_URL = os.getenv("INDYKITE_BASE_URL", "").strip()
CIQ_QUERY_HQ_WEATHER = os.getenv("CIQ_QUERY_HQ_WEATHER", "").strip() or "get-hq-weather"
_HQ_KEYWORDS = ("hq", "headquarters", "head office", "head-office", "canbank office", "the office")
Expand Down Expand Up @@ -278,9 +277,10 @@ async def _mcp_session(access_token: str):
msg = "MCP_SERVER_URL not configured"
raise RuntimeError(msg)

# The MCP server resolves the AppAgent identity server-side from the project's
# MCP server configuration (app_agent_id); the caller sends only the user's
# Bearer token. X-IK-ClientKey is no longer used.
headers: dict[str, str] = {}
if MCP_AUTH_HEADER:
headers["X-IK-ClientKey"] = MCP_AUTH_HEADER
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
if INDYKITE_BASE_URL:
Expand Down
6 changes: 5 additions & 1 deletion a2a/iag-mcp-demo/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ CIQ_QUERY_STORE_DECISION=store-decision
CIQ_QUERY_HQ_WEATHER=get-hq-weather

# [Required] MCP configuration
IK_APP_AGENT_KEY=
# NOTE: the MCP server resolves the AppAgent identity server-side from the
# project's MCP server configuration (app_agent_id), which is created together
# with the project and configured in the IndyKite console. Callers send only the
# user's Bearer token; the previously-required IK_APP_AGENT_KEY / X-IK-ClientKey
# is no longer used.
# Origin (scheme + host) of the real IndyKite MCP server. The MCP-protecting IAG
# (mcp-iag) uses this as its downstream target; the request path is taken from
# the incoming call, so only the origin is needed here.
Expand Down
42 changes: 26 additions & 16 deletions a2a/iag-mcp-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ together via Docker Compose. See
- a ContX IQ knowledge query + policy — pick any pair from
[`bruno/iag-demo/ciq-context`](bruno/iag-demo/ciq-context),
- an App Agent with a credentials token,
- a Token Introspect config pointing at the Curity issuer.
- a Token Introspect config pointing at the Curity issuer,
- the project's **MCP server configuration** `enabled` and bound to that App
Agent (`app_agent_id`) and Token Introspect (`token_introspect_id`). This
config is created together with the project (it can't be created from the
demo); enable and configure it in the IndyKite console. The MCP server
resolves the App Agent server-side from it, so MCP callers no longer send an
App Agent token.
- **Provider clients** for `console` (chatbot login), `indykiteagent`
(orchestrator), `indykiteagent-2` (retriever), and `indykiteagent-3`
(weather) — each with its secret.
Expand All @@ -97,7 +103,7 @@ Fill in, at a minimum:
| `INDYKITE_BASE_URL` | `https://api.eu.indykite.com` or `https://api.us.indykite.com` |
| `CIQ_QUERY_ID` | Knowledge query ID or name from your project |
| `WORKFLOW_ID` | The `external_id` of the single `Workflow` node to whitelist (sets `JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID` in [`iag-base-docker.yaml`](iag-base-docker.yaml)). If unset/removed, all workflows defined in the IKG are considered when authorizing requests. |
| `APP_AGENT_CREDENTIALS_TOKEN` / `IK_APP_AGENT_KEY` | App Agent credentials token |
| `APP_AGENT_CREDENTIALS_TOKEN` | App Agent credentials token used by the gateway for its ContX IQ calls (`JARVIS_CONTX_IQ_APP_AGENT_CREDENTIALS_TOKEN`) |
| `MCP_SERVER_ORIGIN` | Scheme + host of the MCP server, e.g. `https://us.mcp.indykite.com` / `https://eu.mcp.indykite.com`. Used as the downstream target of `mcp-iag`. |
| `MCP_SERVER_PATH` | MCP endpoint path, e.g. `/mcp/v1/<PROJECT_GID_URL_ENCODED>`. The compose file appends this to the `mcp-iag` host that the agents call. |
| `MCP_SERVER_URL` | Direct URL to the MCP server (`<MCP_SERVER_ORIGIN><MCP_SERVER_PATH>`). Kept for reference / bypassing `mcp-iag`; by default the agents are routed through the gateway instead. |
Expand Down Expand Up @@ -215,13 +221,14 @@ How it is wired:
<!-- -->

> [!NOTE]
> The MCP agents authenticate with `IK_APP_AGENT_KEY` (an App Agent token), not
> the chatbot user token used by the A2A flows. For `mcp-iag` to accept those
> calls, that token must be introspectable and pass the AuthZEN check inherited
> from `iag-base` (`JARVIS_AUTHZEN_ACTION: CAN_TRIGGER`, `JARVIS_AUTHZEN_SUBJECT_TYPES: User`).
> If your App Agent isn't modeled as a `User` subject, override
> `JARVIS_AUTHZEN_ACTION` / `JARVIS_AUTHZEN_SUBJECT_TYPES` on the `mcp-iag`
> service to match your graph.
> MCP calls now carry **only** the user's Bearer token — the same chatbot user
> token used by the A2A flows — so `mcp-iag` runs the same AuthZEN check inherited
> from `iag-base` (`JARVIS_AUTHZEN_ACTION: CAN_TRIGGER`, `JARVIS_AUTHZEN_SUBJECT_TYPES: User`),
> with the Bearer token's `sub` as the subject. The downstream IndyKite MCP server
> resolves the App Agent it uses to call IndyKite APIs **server-side**, from the
> project's MCP server configuration (`app_agent_id`) — callers no longer send an
> App Agent token (`IK_APP_AGENT_KEY` / `X-IK-ClientKey`), which the MCP server has
> removed.

**To bypass the gateway** (talk to the MCP server directly, the original
behaviour), set `MCP_SERVER_URL` back to `${MCP_SERVER_URL}` in the `retriever`
Expand Down Expand Up @@ -251,13 +258,16 @@ Quick prompts once you're logged in as **Leslie**:
isn't returning rows. Verify with
[`bruno/iag-demo/authzen/subject-can-trigger-workflow.yml`](bruno/iag-demo/authzen/subject-can-trigger-workflow.yml)
and the matching CIQ query in `bruno/iag-demo/ciq-context`.
- **`401` / `403` on MCP calls (retriever/weather data lookups)** — the App
Agent token (`IK_APP_AGENT_KEY`) isn't being accepted by `mcp-iag`. Confirm
it's introspectable and that the AuthZEN action/subject on `mcp-iag` match how
your App Agent is modeled (see the note in
[Protecting MCP traffic](#protecting-mcp-traffic-mcp-iag)). To isolate whether
the gateway is the cause, temporarily bypass it (set `MCP_SERVER_URL` back to
the direct URL).
- **`401` / `403` on MCP calls (retriever/weather data lookups)** — the user's
Bearer token isn't being accepted. Confirm it's introspectable and bound to the
project's Token Introspect issuer/audience, that it passes the `mcp-iag` AuthZEN
check (`CAN_TRIGGER` / `User`), and that the project has an **enabled MCP server
configuration** with a valid `app_agent_id` (the App Agent is resolved
server-side; a missing/disabled config rejects all MCP requests). A `401` that
returns `.well-known/oauth-protected-resource` metadata means the Bearer token
is missing/expired/wrongly-bound — not a missing App Agent key. To isolate
whether the gateway is the cause, temporarily bypass it (set `MCP_SERVER_URL`
back to the direct URL).
- **Tail gateway logs** to see the introspect / exchange / CIQ / AuthZen
decisions:

Expand Down
2 changes: 0 additions & 2 deletions a2a/iag-mcp-demo/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ services:
# Route MCP calls through the MCP-protecting IAG instead of the MCP server
# directly. To bypass the IAG, set this back to ${MCP_SERVER_URL}.
MCP_SERVER_URL: http://${IAG_MCP_HOST}:${IAG_MCP_PORT}${MCP_SERVER_PATH}
IK_APP_AGENT_KEY: ${IK_APP_AGENT_KEY}
GEMINI_ENABLED: ${GEMINI_ENABLED}
GEMINI_API_KEY: ${GEMINI_API_KEY}
GEMINI_MODEL: ${GEMINI_MODEL}
Expand Down Expand Up @@ -205,7 +204,6 @@ services:
# Route MCP calls through the MCP-protecting IAG instead of the MCP server
# directly. To bypass the IAG, set this back to ${MCP_SERVER_URL}.
MCP_SERVER_URL: http://${IAG_MCP_HOST}:${IAG_MCP_PORT}${MCP_SERVER_PATH}
IK_APP_AGENT_KEY: ${IK_APP_AGENT_KEY}
INDYKITE_BASE_URL: ${INDYKITE_BASE_URL}
CIQ_QUERY_HQ_WEATHER: ${CIQ_QUERY_HQ_WEATHER:-get-hq-weather}
LOG_LEVEL: ${LOG_LEVEL}
Expand Down
1 change: 0 additions & 1 deletion a2a/iag-mcp-demo/retriever_agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ RETRIEVER_PORT=6002
RETRIEVER_AGENT_NAME=retriever_agent
LLM_MODEL=qwen3:14b-q8_0
MCP_SERVER_URL=https://us.mcp.indykite.com/mcp/v1/gid%3AAAAAApo7TvCs_0TQt-WFbP5KxMM
IK_APP_AGENT_KEY=
GEMINI_API_KEY=
GEMINI_MODEL=gemini-2.5-pro
# Use Gemini instead of local Ollama when true (GEMINI_ENABLED or GEMENI_ENABLED)
Expand Down
2 changes: 2 additions & 0 deletions a2a/iag-mcp-demo/retriever_agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ When using the Indykite MCP server (`us.mcp.indykite.com`), the agent applies tw

**Access token**: The MCP `Authorization: Bearer <token>` header is taken from the incoming A2A request's `Authorization` header. Callers (e.g. the orchestrator) must forward the user's token when invoking the retriever.

This Bearer token is the only auth header sent to the MCP server — the App Agent identity is resolved server-side from the project's MCP server configuration (`app_agent_id`), so no `X-IK-ClientKey` App Agent token is sent.

**Base URL** (`INDYKITE_BASE_URL`): When set, sent as `X-IndyKite-Base-URL` on all MCP requests. Use this to target a specific Indykite API region (e.g. `https://us.api.indykite.com`).

## Customization
Expand Down
6 changes: 3 additions & 3 deletions a2a/iag-mcp-demo/retriever_agent/retriever_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ async def _patched_handle_post_request(self, ctx):
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "").strip()
MCP_AUTH_HEADER = os.getenv("IK_APP_AGENT_KEY", "").strip()
INDYKITE_BASE_URL = os.getenv("INDYKITE_BASE_URL", "").strip()
CIQ_QUERY_STOCK_PRICE = os.getenv("CIQ_QUERY_STOCK_PRICE", "").strip() or "get-stock-quote"
CIQ_QUERY_PURCHASE_LIMIT = os.getenv("CIQ_QUERY_PURCHASE_LIMIT", "").strip() or "get-stock-trade-threshold"
Expand Down Expand Up @@ -846,9 +845,10 @@ async def _mcp_session(access_token: str = ""):
yield []
return

# The MCP server resolves the AppAgent identity server-side from the project's
# MCP server configuration (app_agent_id); the caller sends only the user's
# Bearer token. X-IK-ClientKey is no longer used.
headers: dict[str, str] = {}
if MCP_AUTH_HEADER:
headers["X-IK-ClientKey"] = MCP_AUTH_HEADER
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
if INDYKITE_BASE_URL:
Expand Down
1 change: 0 additions & 1 deletion a2a/iag-mcp-demo/weather_agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ LOG_LEVEL=INFO
# ciq_execute against the canbank get-hq-weather knowledge query instead of
# hitting Open-Meteo directly. Other cities always take the direct path.
MCP_SERVER_URL=
IK_APP_AGENT_KEY=
INDYKITE_BASE_URL=
CIQ_QUERY_HQ_WEATHER=get-hq-weather
6 changes: 3 additions & 3 deletions a2a/iag-mcp-demo/weather_agent/weather_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ async def _patched_handle_post_request(self, ctx):
DEFAULT_CITY = os.getenv("WEATHER_DEFAULT_CITY", "London").strip()
WEATHER_TIMEOUT = float(os.getenv("WEATHER_TIMEOUT", "15"))
MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "").strip()
MCP_AUTH_HEADER = os.getenv("IK_APP_AGENT_KEY", "").strip()
INDYKITE_BASE_URL = os.getenv("INDYKITE_BASE_URL", "").strip()
CIQ_QUERY_HQ_WEATHER = os.getenv("CIQ_QUERY_HQ_WEATHER", "").strip() or "get-hq-weather"
_HQ_KEYWORDS = ("hq", "headquarters", "head office", "head-office", "canbank office", "the office")
Expand Down Expand Up @@ -278,9 +277,10 @@ async def _mcp_session(access_token: str):
msg = "MCP_SERVER_URL not configured"
raise RuntimeError(msg)

# The MCP server resolves the AppAgent identity server-side from the project's
# MCP server configuration (app_agent_id); the caller sends only the user's
# Bearer token. X-IK-ClientKey is no longer used.
headers: dict[str, str] = {}
if MCP_AUTH_HEADER:
headers["X-IK-ClientKey"] = MCP_AUTH_HEADER
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
if INDYKITE_BASE_URL:
Expand Down