Back to all tutorials
Agent Gateway

Protect an MCP server with Agent Gateway: Google Drive behind the MCP proxy

Put the IndyKite Agent Gateway (IAG) in MCP proxy mode in front of any MCP server - demonstrated end to end with a Google Drive MCP server in the iag-mcp-demo reference app.

This tutorial is grounded in the iag-mcp-demo reference application. It puts the IndyKite Agent Gateway (IAG) in MCP proxy mode (protected_agent.protocol: mcp) in front of an MCP server - and proves the mode is downstream-agnostic by protecting a Google Drive MCP server that has nothing to do with IndyKite. Every request is authorized against the Identity Knowledge Graph (IKG) before it is forwarded: who the caller is, which workflow they may trigger, and through which chain of agents.

What you will have at the end

  1. A clear mental model of what IAG's MCP proxy mode does and does not do (session pass-through, SSE streaming, header handling, per-request authorization).
  2. A Google Drive MCP server running as a container: the reference stdio server wrapped into MCP Streamable HTTP.
  3. A Workflow node (external_id=wf-drive), an Agent node (indykiteagent-drive), and the CAN_TRIGGER / INVOKES relationships in the IKG.
  4. The CAN_TRIGGER KBAC policy and the ContX IQ query that resolves (workflow, agent_list) pairs for the gateway.
  5. A dedicated gateway instance, drive-mcp-iag, proxying MCP traffic on port 8887.
  6. A full-text Google Drive search executed through the gateway as an authorized user - and the same call denied with 403 for everyone else, with matching audit records.

Prerequisites

  • An IndyKite project with AuthZEN/KBAC and ContX IQ enabled, and an App Agent credentials token.
  • An OAuth2-compliant IdP with introspect, client-credentials, and token-exchange endpoints, holding the demo's agent clients - and millicent as a login user (chapter 7 logs in as her).
  • Docker and Docker Compose, plus a Google account for chapter 3.
  • The base iag-mcp-demo configured per the repo README - this tutorial adds the Drive pieces on top and bootstraps the .env in chapter 7.

Who this tutorial is for

  • Developers exposing MCP servers to AI agents who need token introspection, authorization, and audit in front of them - without changing the MCP server itself.
  • Platform and security engineers who want the same enforcement point for MCP tool calls as for A2A agent calls.
  • AI agents consuming this doc as a runbook - every chapter uses explicit service names, ports, file paths, and commands.

Demo topology at a glance

The iag-mcp-demo runs the full canbank agentic stack. This tutorial focuses on the services below; the Google Drive pair only starts when the drive compose profile is enabled.

Service Role Port
chatbot Web UI; users log in here, and the demo scripts reuse its session token. 3000
mcp-iag IAG in MCP proxy mode in front of the IndyKite MCP server. 8886
drive-mcp-iag IAG in MCP proxy mode in front of the Google Drive MCP server (profile drive). 8887
drive-mcp Google Drive MCP server: stdio reference server wrapped into Streamable HTTP (profile drive). 8000
analyst-iag IAG in front of the analyst agent (used by the optional analyst path in chapter 7). 8885
analyst A2A data analyst agent; can use both the IndyKite and the Drive MCP backends. 6005
1

Chapter 1

What MCP proxy mode is

How one configuration switch turns the Agent Gateway into an authorization-enforcing proxy for any MCP Streamable HTTP server.

Chapter 1: What MCP proxy mode is

The IndyKite Agent Gateway (IAG) protects exactly one downstream per instance. By default (protected_agent.protocol: a2a) that downstream is an A2A agent. Set protected_agent.protocol: mcp and the same gateway instead proxies MCP Streamable HTTP traffic to a downstream MCP server. Everything in front of the forwarding step - token introspection, the CAN_TRIGGER AuthZEN check, delegation-chain validation against the IKG, and audit - is identical in both modes.

For the full product reference, see the official Agent Gateway documentation.

What the MCP proxy does

  • Accepts any MCP method on any path - initialize, the initialized notification, tools/list, tools/call, resources/list, resources/read - and forwards it after the authorization checks pass. Session-teardown DELETE requests are forwarded too; whether teardown is honored is up to the downstream server.
  • Passes the session through untouched - the Mcp-Session-Id header is forwarded in both directions without translation. The session the downstream server mints during initialize is the session the caller uses on every follow-up.
  • Streams SSE responses - the downstream response body is streamed to the caller and flushed per SSE message, so long-running responses are not buffered or truncated.
  • Preserves path and query - protected_agent.base_url is the downstream origin only. The gateway resolves the incoming request path and query string on top of it, so callers use the same path the MCP server expects. Any path segment you put into base_url is discarded.
  • Swaps the credentials - the caller's Authorization header is removed and replaced with a delegation token the gateway mints at the IdP for the protected downstream. Chapter 2 covers what this means for the downstream server.

What the MCP proxy does not do

  • It does not inspect MCP payloads. Authorization is per request and per session ("may this user reach this MCP server through this workflow?"), not per tool. Every tool the downstream exposes is reachable once a request is authorized.
  • It is not a generic HTTP reverse proxy. The downstream must speak MCP Streamable HTTP.

Downstream-agnostic by design

Nothing in MCP proxy mode is specific to IndyKite's own MCP server. The iag-mcp-demo proves this with two protected MCP downstreams:

  • mcp-iag (port 8886) protects the IndyKite MCP server - the demo agents reach ContX IQ and AuthZEN tools through it.
  • drive-mcp-iag (port 8887) protects a Google Drive MCP server - a third-party server that knows nothing about IndyKite. This is the pair this tutorial builds.

Mental model

millicent (user token)
   │  MCP Streamable HTTP: initialize / tools/list / tools/call
   ▼
drive-mcp-iag:8887  ── introspect token        ──>  IdP
   │                 ── CAN_TRIGGER wf-drive   ──>  AuthZEN
   │                 ── workflow chain          ──>  ContX IQ (IKG)
   │                 ── AUTHORIZED / NOT_AUTHORIZED ──>  audit stream
   │  Authorization header replaced with a delegation token,
   │  Mcp-Session-Id and SSE streamed through untouched
   ▼
drive-mcp:8000/mcp  ──>  Google Drive API (server's own Google credentials)

What comes next

Chapter 2 follows one MCP request through the gateway and lists the exact HTTP responses callers can get back.

2

Chapter 2

How the gateway handles an MCP request

The runtime path of one MCP call through IAG, the credential swap, and every HTTP response a caller can receive.

Chapter 2: How the gateway handles an MCP request

Take one call - tools/call with the Drive server's search tool - sent to http://localhost:8887/mcp with a user's Bearer token. The gateway runs the same sequence for every request in the session:

  1. Extract the Bearer token. No Authorization: Bearer header means an immediate 401.
  2. Introspect the token at the IdP. The token must be active and carry a subject. Delegated tokens carry an act claim naming the chain of actors the request has passed through; a request straight from a user has no chain yet.
  3. Resolve the allowed workflows from the IKG. Via the configured ContX IQ query, the gateway asks: for the agent I protect (indykiteagent-drive), which workflows invoke it, and through which agent chain? The answer comes from the Workflow/Agent nodes and INVOKES relationships you model in chapter 5.
  4. Check the subject with AuthZEN. Can this subject CAN_TRIGGER one of those workflows? This evaluates the KBAC policy you create in chapter 5 against the graph.
  5. Validate the delegation chain. The actors in the token's act chain must match one of the workflow's modeled agent chains.
  6. Audit the decision. An AUTHORIZED or NOT_AUTHORIZED record is emitted on the configured audit stream (chapter 8).
  7. Mint the downstream credential and forward. The gateway obtains its own token at the IdP (client credentials plus token exchange, using the client_id/client_secret configured for this instance), replaces the caller's Authorization header with it, and forwards the request - same path, same query, same body, Mcp-Session-Id untouched. The downstream's status code, headers, and (streamed) body go back to the caller as-is.

The credential swap, and what it means for your MCP server

Because the gateway always replaces the Authorization header, the protected MCP server never sees the caller's token - it sees a delegation token identifying the gateway's IdP client. Two consequences:

  • An MCP server that requires the caller's own upstream credential (for example a hosted MCP endpoint that expects the caller's Google OAuth access token) will not work behind the gateway.
  • The downstream must hold its own credentials for whatever backend it talks to. The Google Drive server in this tutorial holds its own Google OAuth material (chapter 3) and simply ignores the incoming Bearer token.

Responses a caller can receive

Status Body / meaning
200 / 202 Authorized and forwarded; this is the downstream MCP server's own response (JSON or SSE).
400 Bad request - the request could not be processed because of its content.
401 {"message": "Missing bearer token"} when no token is sent. Other 401 messages indicate an inactive token or one missing required claims after introspection.
403 {"message": "Authorization check failed"} - the caller is authenticated but the subject cannot trigger the workflow, or the delegation chain does not match any modeled chain.
500 {"message": "Internal Server Error"}.
502 The downstream MCP server could not be reached or the forward failed.

The 401-with-no-token response doubles as a liveness probe: an unauthenticated POST to a gateway port answering {"message":"Missing bearer token"} proves the instance is up and enforcing. Chapter 7 uses exactly this check.

What comes next

Chapter 3 sets up the Google side: a Google Cloud project, the Drive API, and the OAuth material the Drive MCP server needs.

3

Chapter 3

Set up Google Drive access

Create the Google Cloud OAuth client and mint the credentials the Drive MCP server uses to search Google Drive.

Chapter 3: Set up Google Drive access

The Drive MCP server talks to the Google Drive API with its own Google OAuth credentials (chapter 2 explains why the caller's token never reaches it). This chapter produces the two credential files the server needs. Work from the demo directory:

cd developer-hub/a2a/iag-mcp-demo

1. Create a Google Cloud project and enable the Drive API

  1. Go to console.cloud.google.com. A personal Google account is easiest: it can create projects freely, and the demo then searches that account's Drive.
  2. Create a project (for example iag-drive-demo) and switch to it.
  3. Search for Google Drive API and click Enable.

2. Configure the OAuth consent screen

Under APIs & Services → OAuth consent screen: choose type External, fill the required fields, and under Test users add the Google account you will authorize with. While the app is in Testing status only test users can authenticate - anyone else gets "Access blocked: app has not completed verification".

3. Create the OAuth client and download the keys

  1. APIs & Services → Credentials → Create credentials → OAuth client ID → application type Desktop app → Create → Download JSON.
  2. Save the download as drive_mcp/.gdrive/gcp-oauth.keys.json inside the demo directory. This path is git-ignored.

4. Authorize once (browser flow)

Run the one-time auth bootstrap on the host. It opens a browser and writes the refresh-token credentials file next to the keys:

cd drive_mcp
GDRIVE_OAUTH_PATH=$PWD/.gdrive/gcp-oauth.keys.json \
GDRIVE_CREDENTIALS_PATH=$PWD/.gdrive/.gdrive-server-credentials.json \
npx -y @modelcontextprotocol/server-gdrive auth
cd ..

In the browser: pick the account (the project owner or a test user) → on the "Google hasn't verified this app" warning click AdvancedGo to <app> (unsafe) (it is your own app) → Allow read access to Drive. The granted scope is read-only (https://www.googleapis.com/auth/drive.readonly).

5. Verify the credential files

ls -A drive_mcp/.gdrive/
# expect both files (next to the repo's committed .gitkeep):
#   gcp-oauth.keys.json
#   .gdrive-server-credentials.json

Both files must come from the same OAuth client. If you ever replace gcp-oauth.keys.json, re-run step 4 - mismatched files fail later with invalid_request on the first Drive call.

6. Seed some content

Put a few files in that Google account's Drive so searches return something. The Drive server's search tool is full-text over file contents, so a reliable trick is to create a Google Doc containing a unique word (for example canbank-test-fixture) - searching for that word then always returns exactly that document.

What comes next

Chapter 4 packages the Drive MCP server as a container the gateway can protect.

4

Chapter 4

The Google Drive MCP server container

Wrap the reference stdio Drive MCP server into MCP Streamable HTTP so the gateway can proxy it.

Chapter 4: The Google Drive MCP server container

The gateway proxies MCP Streamable HTTP, but the reference Google Drive MCP server speaks stdio. The demo bridges the two with supergateway, a small adapter that spawns the stdio server and exposes it as a stateful Streamable HTTP endpoint. This is a general recipe: any stdio MCP server can be put behind IAG the same way.

The image

The drive_mcp/Dockerfile in the demo installs supergateway and the vendored Drive server, then starts:

supergateway \
  --stdio "node /app/vendor/index.js" \
  --outputTransport streamableHttp \
  --stateful \
  --port 8000 \
  --streamableHttpPath /mcp \
  --healthEndpoint /healthz
  • MCP endpoint: http://drive-mcp:8000/mcp. This is the path callers will also use on the gateway, since the gateway forwards the incoming path onto the downstream origin.
  • --stateful: supergateway mints an Mcp-Session-Id per initialize; the gateway passes it through both ways.
  • Health check: GET /healthz.

The compose service

drive-mcp:
  image: drive-mcp:latest
  profiles: ["drive"]
  ports:
    - "8000:8000"
  networks:
    - drive-mcp-iag-network
  environment:
    # Paths to the mounted Google OAuth material (values are paths, not secrets).
    GDRIVE_OAUTH_PATH: /gdrive/gcp-oauth.keys.json
    GDRIVE_CREDENTIALS_PATH: /gdrive/.gdrive-server-credentials.json
  volumes:
    - ./drive_mcp/.gdrive:/gdrive:ro

The credential files from chapter 3 are mounted read-only. The server refreshes its Google access token from them automatically; the Bearer token the gateway injects on forwarded requests is simply ignored - exactly the arrangement chapter 2 requires.

What the server exposes

Capability Behavior
Tool search Full-text search over file contents in the authorized account's Drive; returns up to 10 matches, one <fileId> <name> (<mimeType>) line each.
resources/list Lists Drive files as resources with gdrive:///<fileId> URIs.
resources/read Reads one file by URI. Google-native files (Docs, Sheets, Slides) are exported as text; other formats come back base64-encoded, so point read prompts at Google-native copies.

Build it

make new-drive-mcp     # builds the drive-mcp image from ./drive_mcp

What comes next

The server is ready but nothing authorizes access to it yet. Chapter 5 models who may reach it, and through which chain, in the IKG.

5

Chapter 5

Model the wf-drive workflow in the IKG

The Workflow and Agent nodes, CAN_TRIGGER grant, and INVOKES chain that authorize Google Drive access - plus one workflow per call shape.

Chapter 5: Model the wf-drive workflow in the IKG

The gateway authorizes an MCP request only if (a) the subject can CAN_TRIGGER a workflow that invokes the protected agent and (b) the request's delegation chain matches that workflow's modeled agent chain. Both facts live in the Identity Knowledge Graph (IKG). This chapter adds them for the Drive downstream.

1. Register the IdP client

Create a machine-to-machine client indykiteagent-drive in your IdP (client credentials and token exchange flows; no redirect URI). The client ID must exactly match the Agent node's external_id - the gateway matches delegation-chain actors against agent IDs from the graph.

2. Create the nodes

Via the Capture API (POST /capture/v1/nodes) - one Workflow and one Agent:

{
  "nodes": [
    { "external_id": "wf-drive", "type": "Workflow", "is_identity": false, "properties": [] },
    { "external_id": "indykiteagent-drive", "type": "Agent", "is_identity": false, "properties": [] }
  ]
}

3. Create the relationships

Via POST /capture/v1/relationships. Two edges matter: the grant (who may trigger the workflow) and the chain (which agent the workflow invokes). Every INVOKES edge in a chain must carry a workflow_name property equal to the workflow's external_id - the workflow-resolution query filters on it at every hop and silently drops chains without it.

{
  "relationships": [
    {
      "source": { "type": "User", "external_id": "millicent" },
      "target": { "type": "Workflow", "external_id": "wf-drive" },
      "properties": [],
      "type": "CAN_TRIGGER"
    },
    {
      "source": { "type": "Workflow", "external_id": "wf-drive" },
      "target": { "type": "Agent", "external_id": "indykiteagent-drive" },
      "properties": [ { "type": "workflow_name", "value": "wf-drive" } ],
      "type": "INVOKES"
    }
  ]
}

In the demo dataset only millicent gets this grant. Every other user is denied at the gateway - that asymmetry is the demo.

4. Create the KBAC policy

The gateway's AuthZEN check needs one policy answering "can this subject trigger this workflow?". It is generic over all workflows - if you already provisioned it for other gateway instances, there is nothing to add; wf-drive is covered the moment it exists in the graph.

{
  "meta": {
    "policyVersion": "1.0-kbac"
  },
  "subject": {
    "type": "User"
  },
  "actions": [
    "CAN_TRIGGER"
  ],
  "resource": {
    "type": "Workflow"
  },
  "condition": {
    "cypher": "MATCH (subject)-[:CAN_TRIGGER]->(resource:Workflow)"
  }
}

The subject type must be listed in the gateway's authzen.subject_types setting (the demo configures User). If you use more subject types, create one policy per type.

The direct-edge condition above is all wf-drive needs. The demo dataset provisions a department-aware variant - MATCH (subject:User)-[:WORKS_IN|CAN_TRIGGER*..3]->(resource:Workflow) - so users also inherit workflow access through their department (a User -WORKS_IN-> Department -CAN_TRIGGER-> Workflow path). That is how the support and trading staff reach wf1 in chapter 7's persona prompts.

5. Create the workflow-resolution ContX IQ query

The gateway's second question - "which workflows invoke the agent I protect, through which chains?" - is answered by a ContX IQ query (the demo names it get-agent-workflows). Given $agent_id, it returns one (workflow, agent_list) pair per allowed chain, keeping only chains whose every hop carries the matching workflow_name:

{
  "meta": {
    "policy_version": "1.0-ciq"
  },
  "subject": {
    "type": "_Application"
  },
  "condition": {
    "cypher": "MATCH (subject:_Application) MATCH (wf:Workflow)-[rels:INVOKES*]->(a:Agent {external_id: $agent_id}) WHERE ALL(r IN rels WHERE r.workflow_name = wf.external_id AND endNode(r):Agent) WITH subject, wf.external_id AS workflow, [r IN rels | endNode(r).external_id] AS agent_list",
    "filter": []
  },
  "allowed_reads": {
    "nodes": [],
    "relationships": [],
    "aggregate_values": [
      "workflow",
      "agent_list"
    ]
  }
}

This query is also shared by every gateway instance - record its name or GID once; it goes into the gateway configuration as the ContX IQ query ID (chapter 6). With the graph from this chapter fully modeled, running it for agent_id = indykiteagent-drive returns one row per call shape (the two extra shapes are modeled in the next section):

{
  "data": [
    { "aggregate_values": { "workflow": "wf-drive", "agent_list": ["indykiteagent-drive"] } },
    { "aggregate_values": { "workflow": "wf-drive-analyst", "agent_list": ["indykiteagent-4", "indykiteagent-drive"] } },
    { "aggregate_values": { "workflow": "wf-drive-console", "agent_list": ["indykiteagent", "indykiteagent-4", "indykiteagent-drive"] } }
  ]
}

One workflow per call shape

The gateway resolves one agent chain per workflow. If the same protected agent should be reachable through several different chains - directly, via another agent, via a console orchestrator - model each shape as its own workflow. The demo wires four:

Workflow Chain Used by
wf-drive indykiteagent-drive Direct MCP calls to the Drive gateway (chapter 7).
wf-drive-analyst indykiteagent-4 → indykiteagent-drive A prompt to the analyst agent, which calls Drive as an MCP backend.
wf-drive-console indykiteagent → indykiteagent-4 → indykiteagent-drive The chatbot console: orchestrator → analyst → Drive.
wf3-console indykiteagent → indykiteagent-4 → indykiteagent-mcp The console-routed analyst reaching the IndyKite MCP server.

When two workflows share an agent-to-agent hop (for example indykiteagent-4 → indykiteagent-drive in both the analyst and console shapes), create parallel INVOKES edges - one per workflow - and add a discriminating_property property with value workflow_name to each, so the edges stay distinct per workflow:

{
  "source": { "type": "Agent", "external_id": "indykiteagent-4" },
  "target": { "type": "Agent", "external_id": "indykiteagent-drive" },
  "properties": [
    { "type": "workflow_name", "value": "wf-drive-analyst" },
    { "type": "discriminating_property", "value": "workflow_name" }
  ],
  "type": "INVOKES"
}

Grant CAN_TRIGGER per shape as well - in the demo, millicent holds it on all three wf-drive* workflows.

Provisioning shortcut

The canbank-iag companion app provisions this entire dataset - nodes, relationships, the KBAC policy, and the ContX IQ queries - through pre-filled forms, and the demo repo carries the same payloads as a Bruno collection (bruno/iag-demo/ingest/agent-workflow). Both are safe to re-run: ingestion is an upsert.

What comes next

Chapter 6 configures the gateway instance that enforces all of this: drive-mcp-iag.

6

Chapter 6

Configure the drive-mcp-iag gateway

The per-instance settings that switch IAG into MCP proxy mode and point it at the Drive server.

Chapter 6: Configure the drive-mcp-iag gateway

The gateway takes its configuration from a YAML file passed with --config, from environment variables, or both. Every config path maps to an environment variable with the fixed JARVIS_ prefix: uppercase the path and replace dots with underscores, so protected_agent.protocol becomes JARVIS_PROTECTED_AGENT_PROTOCOL. The demo uses a shared base service (iag-base-docker.yaml) holding the common IdP, AuthZEN, ContX IQ, and audit settings, and each instance overrides only its per-instance fields. Use a gateway image with MCP proxy support - the demo pins indykite/agent-gateway:2.21.1.

The compose service

drive-mcp-iag:
  extends:
    service: iag-base
    file: iag-base-docker.yaml
  profiles: ["drive"]
  ports:
    - "8887:8887"
  networks:
    - drive-mcp-iag-network
  environment:
    JARVIS_SERVICE_NAME: drive-mcp-iag
    JARVIS_SERVICE_PORT: 8887
    # Switch from the default "a2a" proxy into MCP proxy mode.
    JARVIS_PROTECTED_AGENT_PROTOCOL: mcp
    # Origin only - the gateway forwards the incoming path (/mcp) on top of it.
    JARVIS_PROTECTED_AGENT_BASE_URL: http://drive-mcp:8000
    JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_ID: ${DRIVE_MCP_IDP_CLIENT_ID:-indykiteagent-drive}
    JARVIS_PROTECTED_AGENT_AUTHENTICATION_CLIENT_SECRET: ${DRIVE_MCP_IDP_CLIENT_SECRET}
    # Empty = allow any workflow resolved from the graph. Required here because
    # three call shapes (wf-drive, wf-drive-analyst, wf-drive-console) converge
    # on this gateway; set a single workflow id to pin one shape only.
    JARVIS_CONTX_IQ_ALLOWED_WORKFLOW_ID: ${DRIVE_WORKFLOW_ID:-}
  # Audit delivery is configured per instance via a mounted config file.
  volumes:
    - ./audit-config.yaml:/app/.configs/audit-config.yaml
  command: ["--config=/app/.configs/audit-config.yaml"]

The per-instance fields

Setting Value for this instance
service.name drive-mcp-iag - also stamped on every audit record.
service.port 8887.
protected_agent.protocol mcp. Anything other than a2a or mcp fails startup with invalid protected_agent protocol.
protected_agent.base_url http://drive-mcp:8000 - origin only, no path.
protected_agent.authentication.client_id / client_secret The indykiteagent-drive IdP client from chapter 5.
contx_iq.allowed_workflow_id Empty, so all workflows resolved from the graph authorize - this gateway serves three call shapes.

The IdP, AuthZEN (action CAN_TRIGGER, subject type User), and ContX IQ settings (the get-agent-workflows query ID and App Agent credentials token) are inherited from the shared base and identical to the other gateway instances. Audit delivery comes from the mounted audit-config.yaml shown above - the same file every gateway in the demo mounts; without it the instance runs with auditing disabled, and chapter 8 has nothing to read.

.env entries

# Include the two drive services in a plain `docker compose up`
# (equivalent to `docker compose --profile drive up`); leave empty for the base demo.
COMPOSE_PROFILES=drive

# IdP client for drive-mcp-iag's client-credentials / token-exchange flow.
DRIVE_MCP_IDP_CLIENT_ID=indykiteagent-drive
DRIVE_MCP_IDP_CLIENT_SECRET=<secret from your IdP>

# Empty = all wf-drive* call shapes authorize; set to wf-drive to pin direct calls only.
DRIVE_WORKFLOW_ID=

# Optional: give the analyst agent both MCP backends (alias=url pairs).
# Tool names get prefixed per backend: indykite_ciq_execute, drive_search, ...
ANALYST_MCP_SERVER_URLS=indykite=http://mcp-iag:8886/mcp/v1/<PROJECT_GID_URL_ENCODED>,drive=http://drive-mcp-iag:8887/mcp

What comes next

Chapter 7 starts the stack and runs a Google Drive search through the gateway.

7

Chapter 7

Run a Google Drive search through the gateway

Start the stack, establish an MCP session against drive-mcp-iag, and execute the full-text search - by script, by hand, and by prompt.

Chapter 7: Run a Google Drive search through the gateway

1. Configure the environment

First run only: bootstrap .env and fill in the base values per the repo README - the IndyKite base URL, the get-agent-workflows query ID, the App Agent credentials token, the IdP client secrets, and an LLM key for the agents - then add the Drive entries from chapter 6 (including COMPOSE_PROFILES=drive).

cd developer-hub/a2a/iag-mcp-demo
cp .example.env .env
# edit .env: base demo values per the repo README + the chapter 6 drive entries

2. Build and start the stack

make                   # builds the chatbot and agent images
make new-drive-mcp     # builds the drive-mcp image (chapter 4)
docker compose up -d   # COMPOSE_PROFILES=drive in .env pulls in the two drive services
docker compose ps

With the drive profile enabled expect 12 containers. Confirm the drive pair and prove the gateway is enforcing:

docker compose logs drive-mcp | tail        # the wrapped server is listening on :8000, endpoint /mcp
docker compose logs drive-mcp-iag | tail    # the gateway is listening on :8887

curl -s -X POST http://localhost:8887/mcp -H "Content-Type: application/json" -d '{}'
# {"message":"Missing bearer token"}   <- alive and enforcing (chapter 2)

3. Get an authorized user token

Open http://localhost:3000 (use localhost, not 127.0.0.1) in a fresh incognito window and log in as millicent - the one user granted CAN_TRIGGER on wf-drive in chapter 5. She must exist as a login user in your IdP; if she does not, either create her there or grant your own login user the CAN_TRIGGER edges instead. The demo's test scripts extract the logged-in user's access token from the chatbot session automatically; for the manual calls below, export it as TOKEN. Tokens are short-lived, so run the calls soon after logging in.

4. The script version

./test-drive.sh                       # initialize → tools/list → resources/list → search, as millicent
DRIVE_QUERY=budget ./test-drive.sh    # different search term
./test-drive.sh <user>                # as a different logged-in user

5. The manual version

An MCP session against the gateway, step by step. All requests go to the gateway (:8887/mcp), never to the Drive server directly:

DRIVE="http://localhost:8887/mcp"
H=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
   -H "Accept: application/json, text/event-stream")

# 1. initialize - capture the MCP session id from the response headers
SID=$(curl -s -D - -o /dev/null "${H[@]}" -X POST $DRIVE \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"demo","version":"1.0"}}}' \
  | grep -i mcp-session-id | tr -d '\r' | awk '{print $2}')
echo "session: $SID"      # a UUID; empty means check the token or the gateway logs

# 2. initialized notification (expect HTTP 202)
curl -s -o /dev/null -w "initialized -> HTTP %{http_code}\n" "${H[@]}" \
  -H "Mcp-Session-Id: $SID" -X POST $DRIVE \
  -d '{"jsonrpc":"2.0","method":"initialized","params":{}}'

# 3. list the Drive server's tools (expect: search)
curl -s "${H[@]}" -H "Mcp-Session-Id: $SID" -X POST $DRIVE \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# 4. the search - full-text over the authorized account's Drive
curl -s -m 45 "${H[@]}" -H "Mcp-Session-Id: $SID" -X POST $DRIVE \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search","arguments":{"query":"canbank"}}}'

Expected result of the search - up to 10 lines of <fileId> <name> (<mimeType>); your files will differ, and matches are files whose content mentions the query:

Found 3 files:
1AbC...xYz Financial report (application/vnd.google-apps.document)
1DeF...uVw Retail campaign (application/vnd.google-apps.document)
1GhI...rSt video1.mp4 (video/mp4)

Cross-check: open drive.google.com as the authorized Google account and type the same query into the search bar - same files. Between steps, note what just happened: the gateway introspected millicent's token, confirmed CAN_TRIGGER wf-drive against the graph, audited the decision, swapped in its delegation token, and streamed the Drive server's response back - while the Mcp-Session-Id minted downstream flowed through untouched.

6. The prompt versions (optional)

The same authorization guards the longer chains modeled in chapter 5:

  • Via the analyst (wf-drive-analyst): with ANALYST_MCP_SERVER_URLS set (chapter 6), send the analyst gateway (:8885) an A2A message/send with a prompt like "Search Google Drive for canbank" - the analyst then calls Drive as one of its MCP backends. Cross-backend prompts work too: "Search Google Drive for canbank, then check which internal policy documents mention the same topics." (The repo's ./demo-analyst-drive.sh is a related smoke test: it sends the analyst a hello message and then runs the direct Drive search from step 5.)
  • Via the chatbot console (wf-drive-console): as millicent, type "Search Google Drive for canbank and list the matching files" at http://localhost:3000. Every hop - orchestrator, analyst, Drive - passes its own gateway and leaves its own audit record.

Prompt tips: Drive search is full-text, so to target a specific file prefer listing then reading ("Read the file 'X' from Google Drive and summarize it"), and point read prompts at Google-native documents (chapter 4). For workflow prompts, name the agent ("Use the retriever to …", "Ask the retriever for …") so the orchestrator actually delegates instead of answering itself.

7. Prompts by persona

The graph gives every demo user different access, so the same prompt can succeed for one login and be denied - or return nothing - for another. That asymmetry is the demo. Switch personas with a fresh incognito window per user. Chatbot console prompts:

Persona (access) Prompts that work What is denied
millicent - support dept; wf1, wf3, all wf-drive* Everything leslie can, plus the star turn: "Search Google Drive for canbank and list the matching files.", "List the files in my Google Drive.", "Read the file 'X' from Google Drive and summarize it." Stock prompts return nothing - trading-department data, correct authorization rather than a failure.
leslie - support dept → wf1 "Use the retriever to find internal policy documents about refunds.", "Ask the retriever which past decisions incorporated the refund_policy document.", "What's the weather in London?" Drive and analyst calls → 403; stock prompts return nothing.
roy - trading dept → wf1 "Use the retriever to get the NVDA stock price.", "Ask the retriever how many shares of NVDA the customer rebecca can purchase." Drive and analyst calls → 403.
rebecca - customer, direct wf1 grant "Who am I?", "Show my profile", prompts about her own accounts and documents. Department-scoped queries (internal docs, stock) return nothing - she has no department; Drive → 403.
carol - wf1 only Normal wf1 prompts (as leslie). The designated deny persona: millicent's exact analyst and Drive requests with carol's token → 403. The "same request, different user, different outcome" money shot.
jane - wf2 only Weather prompts: "What's the weather in London?", "What's the weather at CanBank HQ?" All wf1 flows → 403 - she cannot trigger the orchestrator workflow.
joe - plain wf1 Baseline wf1 prompts; handy as the default subject for a direct AuthZEN CAN_TRIGGER evaluation. Analyst and Drive → 403.

Note the two kinds of "no" in the table: a 403 is the gateway denying the workflow; an empty result is data-level scoping inside an authorized workflow (the retriever's queries respect department reach). Which personas can actually log in depends on your IdP - the graph side comes from the chapter 5 ingest; if a persona cannot log in, create them in the IdP or grant your login user the equivalent edges.

What comes next

A successful search is only half the story. Chapter 8 proves the denials and reads the audit trail.

8

Chapter 8

Verify denials and read the audit trail

Force NOT_AUTHORIZED outcomes on purpose, read the audit records, and fix the common failure modes.

Chapter 8: Verify denials and read the audit trail

Confidence in an enforcement point comes from watching it say no. Tail the gateway while you test:

docker compose logs -f drive-mcp-iag

The money shot: same request, different user

Log in as any user other than millicent (fresh incognito window) and repeat chapter 7's calls - ./test-drive.sh leslie, or the manual sequence with that user's token. Every request now returns:

HTTP 403
{"message": "Authorization check failed"}

Only millicent holds CAN_TRIGGER on wf-drive in the graph. Nothing else changed: same gateway, same MCP server, same request body - different subject, different decision.

Other denial paths worth forcing once

  • Skip the chain - call the Drive gateway with a token delegated for a different workflow: the actors in the token's act chain match no wf-drive* chain → 403.
  • Break the model - remove the workflow_name property from the INVOKES edge: the workflow-resolution query stops returning the chain → 403.
  • Revoke the grant - delete the CAN_TRIGGER edge: AuthZEN answers no → 403.

Reading the audit records

Every decision emits one record on the audit stream configured in the mounted audit config (chapter 6): delivered either to a webhook (audit.delivery: webhook plus the target URL and auth) or to a file (audit.delivery: file with a storage path, csv/json/txt format, and size- or time-based rotation). The demo posts records to a webhook on the chatbot, which displays them live. The record fields:

Field Meaning
decision AUTHORIZED or NOT_AUTHORIZED.
reason Human-readable explanation, e.g. subject can trigger workflows wf-drive and the actors in the delegation chain, or on denial subject is not authorized to trigger any of the workflows ... / no workflow matches the actors chain.
subject The end user the request acts for (e.g. millicent).
actor / actorsChain The immediate caller and the full ordered delegation chain from the token's act claim.
action The action evaluated for the decision.
service The gateway instance that decided (drive-mcp-iag) - how you tell the hops apart in a multi-gateway chain.
timestamp / traceID RFC 3339 UTC time and the trace ID correlating gateway, downstream, and client logs.

On a console-path request (chapter 7, wf-drive-console) you should see AUTHORIZED records from every gateway the request traversed - orchestrator, analyst, and Drive - each with a growing actorsChain.

Troubleshooting

Symptom Cause / fix
401 Missing bearer token on your own calls Token missing or expired - tokens are short-lived; log in again and re-export TOKEN.
403 for a user you expected to pass The subject lacks CAN_TRIGGER on the workflow, or the graph model is missing an edge or its workflow_name - re-run the ingest from chapter 5.
Empty Mcp-Session-Id after initialize Check docker compose logs drive-mcp-iag drive-mcp - usually the graph ingest or the IdP client for indykiteagent-drive is missing.
404 on MCP methods The gateway image predates MCP proxy support - use the version pinned in chapter 6 or newer.
Search fails with invalid_request or credential errors The two Google files in drive_mcp/.gdrive/ come from different OAuth clients or are missing - redo chapter 3 step 4.
File reads answer "binary document" Only Google-native files export as text (chapter 4) - convert the file to a Google Doc.

Where you are now

A third-party MCP server is running behind the Agent Gateway with per-user, graph-backed authorization and a full audit trail - and the MCP server itself never changed. The same recipe applies to any MCP Streamable HTTP server: wrap it if it speaks stdio (chapter 4), model who may reach it and through which chain (chapter 5), and point one gateway instance at it (chapter 6).