From 0ca53f977cc3d7a92fc00dd36ce65443734456c3 Mon Sep 17 00:00:00 2001 From: Cowan Macady Date: Thu, 18 Jun 2026 19:57:47 +0200 Subject: [PATCH] feat: update iag apps after mcp modif implement [ENG-8488] --- a2a/iag-demo/.example.env | 5 ++- a2a/iag-demo/README.md | 2 +- a2a/iag-demo/docker-compose.yaml | 2 - a2a/iag-demo/retriever_agent/.env.example | 1 - .../retriever_agent/retriever_agent.py | 6 +-- a2a/iag-demo/weather_agent/.env.example | 1 - a2a/iag-demo/weather_agent/weather_agent.py | 6 +-- a2a/iag-mcp-demo/.example.env | 6 ++- a2a/iag-mcp-demo/README.md | 42 ++++++++++++------- a2a/iag-mcp-demo/docker-compose.yaml | 2 - a2a/iag-mcp-demo/retriever_agent/.env.example | 1 - a2a/iag-mcp-demo/retriever_agent/README.md | 2 + .../retriever_agent/retriever_agent.py | 6 +-- a2a/iag-mcp-demo/weather_agent/.env.example | 1 - .../weather_agent/weather_agent.py | 6 +-- 15 files changed, 50 insertions(+), 39 deletions(-) diff --git a/a2a/iag-demo/.example.env b/a2a/iag-demo/.example.env index e890e3a..dc84d35 100644 --- a/a2a/iag-demo/.example.env +++ b/a2a/iag-demo/.example.env @@ -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/ # [Required] From your IdP Provider configuration diff --git a/a2a/iag-demo/README.md b/a2a/iag-demo/README.md index 77ca060..ae30bf7 100644 --- a/a2a/iag-demo/README.md +++ b/a2a/iag-demo/README.md @@ -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/` `https://eu.mcp.indykite.com/mcp/v1/` | | `CHATBOT_IDP_CLIENT_ID` / `_SECRET` | IdP Provider `console` client | | `ORCHESTRATOR_IDP_CLIENT_ID` / `_SECRET` | IdP Provider `indykiteagent` client | diff --git a/a2a/iag-demo/docker-compose.yaml b/a2a/iag-demo/docker-compose.yaml index 05b47bc..eff5985 100644 --- a/a2a/iag-demo/docker-compose.yaml +++ b/a2a/iag-demo/docker-compose.yaml @@ -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} @@ -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} diff --git a/a2a/iag-demo/retriever_agent/.env.example b/a2a/iag-demo/retriever_agent/.env.example index 2b1bb13..12b4e8f 100644 --- a/a2a/iag-demo/retriever_agent/.env.example +++ b/a2a/iag-demo/retriever_agent/.env.example @@ -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) diff --git a/a2a/iag-demo/retriever_agent/retriever_agent.py b/a2a/iag-demo/retriever_agent/retriever_agent.py index 2d278e9..d0e1873 100644 --- a/a2a/iag-demo/retriever_agent/retriever_agent.py +++ b/a2a/iag-demo/retriever_agent/retriever_agent.py @@ -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" @@ -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: diff --git a/a2a/iag-demo/weather_agent/.env.example b/a2a/iag-demo/weather_agent/.env.example index 2bef4a7..3f82d76 100644 --- a/a2a/iag-demo/weather_agent/.env.example +++ b/a2a/iag-demo/weather_agent/.env.example @@ -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 diff --git a/a2a/iag-demo/weather_agent/weather_agent.py b/a2a/iag-demo/weather_agent/weather_agent.py index fa2322b..446cce7 100644 --- a/a2a/iag-demo/weather_agent/weather_agent.py +++ b/a2a/iag-demo/weather_agent/weather_agent.py @@ -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") @@ -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: diff --git a/a2a/iag-mcp-demo/.example.env b/a2a/iag-mcp-demo/.example.env index d375db6..37a541c 100644 --- a/a2a/iag-mcp-demo/.example.env +++ b/a2a/iag-mcp-demo/.example.env @@ -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. diff --git a/a2a/iag-mcp-demo/README.md b/a2a/iag-mcp-demo/README.md index abfbf78..6877de0 100644 --- a/a2a/iag-mcp-demo/README.md +++ b/a2a/iag-mcp-demo/README.md @@ -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. @@ -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/`. The compose file appends this to the `mcp-iag` host that the agents call. | | `MCP_SERVER_URL` | Direct URL to the MCP server (``). Kept for reference / bypassing `mcp-iag`; by default the agents are routed through the gateway instead. | @@ -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` @@ -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: diff --git a/a2a/iag-mcp-demo/docker-compose.yaml b/a2a/iag-mcp-demo/docker-compose.yaml index 2ecfe88..8979203 100644 --- a/a2a/iag-mcp-demo/docker-compose.yaml +++ b/a2a/iag-mcp-demo/docker-compose.yaml @@ -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} @@ -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} diff --git a/a2a/iag-mcp-demo/retriever_agent/.env.example b/a2a/iag-mcp-demo/retriever_agent/.env.example index 2b1bb13..12b4e8f 100644 --- a/a2a/iag-mcp-demo/retriever_agent/.env.example +++ b/a2a/iag-mcp-demo/retriever_agent/.env.example @@ -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) diff --git a/a2a/iag-mcp-demo/retriever_agent/README.md b/a2a/iag-mcp-demo/retriever_agent/README.md index 5f6487a..78438f9 100644 --- a/a2a/iag-mcp-demo/retriever_agent/README.md +++ b/a2a/iag-mcp-demo/retriever_agent/README.md @@ -62,6 +62,8 @@ When using the Indykite MCP server (`us.mcp.indykite.com`), the agent applies tw **Access token**: The MCP `Authorization: Bearer ` 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 diff --git a/a2a/iag-mcp-demo/retriever_agent/retriever_agent.py b/a2a/iag-mcp-demo/retriever_agent/retriever_agent.py index 2d278e9..d0e1873 100644 --- a/a2a/iag-mcp-demo/retriever_agent/retriever_agent.py +++ b/a2a/iag-mcp-demo/retriever_agent/retriever_agent.py @@ -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" @@ -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: diff --git a/a2a/iag-mcp-demo/weather_agent/.env.example b/a2a/iag-mcp-demo/weather_agent/.env.example index 2bef4a7..3f82d76 100644 --- a/a2a/iag-mcp-demo/weather_agent/.env.example +++ b/a2a/iag-mcp-demo/weather_agent/.env.example @@ -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 diff --git a/a2a/iag-mcp-demo/weather_agent/weather_agent.py b/a2a/iag-mcp-demo/weather_agent/weather_agent.py index fa2322b..446cce7 100644 --- a/a2a/iag-mcp-demo/weather_agent/weather_agent.py +++ b/a2a/iag-mcp-demo/weather_agent/weather_agent.py @@ -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") @@ -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: