Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Deploy, evaluate, and manage AI agents end-to-end on Microsoft Azure AI Foundry
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
foundry-agent/create/references/foundry-tool-catalog.md
1# Foundry Tool Catalog β Project Connections for Remote Tools23Reference for wiring a **remote tool** (catalog tile or generic MCP server) into a Foundry project as a `RemoteTool` project connection, so a toolbox can attach to it.45> π¦ **Toolbox creation gate:** before creating a toolbox/connection, you MUST read the boundary rules in [create-hosted.md β Toolbox creation boundary](../create-hosted.md#toolbox-creation-boundary) and follow them, then continue with the rest of this file.67Three catalog backends cooperate: the **asset-gallery** index discovers connectors, the Logic Apps **managedApis** GET supplies OAuth metadata, and the Logic Apps **apiOperations** GET supplies the operation list and input schemas. Skip these calls only for fully BYO `generic_mcp` servers β every catalog-MCP or connector-namespace flow needs all three.89> π For the toolbox MCP endpoint, protocol, and testing, see [toolbox-reference.md](toolbox-reference.md).10> π For prompt-agent MCP wiring (without a toolbox), see [tool-mcp.md](tool-mcp.md).1112## When to use this reference1314Use when the user mentions any of:1516- *Build β Tools β Connect a tool* (any subtab β Configured, Catalog, Custom)17- "Tool connection", "Remote MCP", "Catalog tile", "Custom Β· Preview"18- A specific catalog tile (GitHub, Box, Pipedrive, monday.com, Microsoft Learn, β¦)19- `RemoteTool` connection, `gateway_connector`, `catalog_MCP`, `generic_mcp`20- **Connector Namespace** / managed MCP server (powered by the Connector Namespace)21- "Bring my own OAuth App" (BYO `client_id` + `client_secret`) for a catalog connector22- Discovering connector operations (`x-ms-operations` / Logic Apps `apiOperations`) or trigger support (`x-ms-trigger`) via the catalog APIs2324Do **not** use for: non-tool connections (Azure OpenAI, AI Search account, Storage), or general toolbox CRUD beyond the attach-and-verify recipe below.2526## Inputs to gather upfront2728Before generating any PUT body, ask the user in one batched question for:29301. **Subscription id**312. **Resource group**323. **Cognitive Services account name** (the Foundry account)334. **Project name** (under the account)345. **Connection name** β lowercase, `[a-z0-9-]`, β€ 24 chars (e.g. `box-1`, `gh-byo`)356. **Tool scenario in plain language** β e.g. "list my files in Box", "create issues on GitHub". Map this onto operations from the connector's `apiOperations` catalog for `gateway_connector`, or onto the catalog MCP server's `tools/list` for `catalog_MCP` / BYO.367. **Toolbox name** to attach into for verification (defaults to `default-tb`)378. **Secrets** (BYO `clientId` / `clientSecret`, `CustomKeys` header value, β¦) β ask the user to **type these directly into the terminal**, never via tooling that echoes them3839The caller's AAD `oid` / `tid` (needed only for the consent-link step) are auto-discovered via `az ad signed-in-user show --query id -o tsv` and `az account show --query tenantId -o tsv`. For a service-principal caller, use `az ad sp show --id <appId>` instead. These values can also be read from the `oid` / `tid` claims on the ARM bearer token; the gateway validates the caller principal owns them.4041## ARM endpoint (shared by every variant)4243```44PUT https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}45/providers/Microsoft.CognitiveServices/accounts/{acct}46/projects/{proj}/connections/{name}?api-version=2025-04-01-preview47```4849### Preflight RBAC5051Caller needs **Azure AI Developer** or **Cognitive Services Contributor** on the project scope. Run this before the first PUT to surface 403s early:5253```pwsh54$oid = az ad signed-in-user show --query id -o tsv55$projId = "/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$acct/projects/$proj"56az role assignment list --assignee $oid --scope $projId --all `57--query "[?roleDefinitionName=='Azure AI Developer' || roleDefinitionName=='Cognitive Services Contributor'].roleDefinitionName" -o tsv58```5960Empty output β caller lacks the required role; expect `403 AuthorizationFailed` on PUT until granted.6162### Common request template6364```pwsh65$tok = az account get-access-token --resource "https://management.azure.com" --query accessToken -o tsv66$h = @{ Authorization = "Bearer $tok"; "Content-Type" = "application/json" }67$uri = "https://management.azure.com/subscriptions/$sub/resourceGroups/$rg/providers/Microsoft.CognitiveServices/accounts/$acct/projects/$proj/connections/${connName}?api-version=2025-04-01-preview"68Invoke-WebRequest -Method PUT -Headers $h -UseBasicParsing -Body $body -Uri $uri69```7071### Body invariants7273- `properties.target` is **required** for every `authType` (validation rejects empty). The exact value depends on the variant β see each body shape. For `gateway_connector` specifically, the literal string `"https://placeholder"` is the correct value on PUT #1 and is **rewritten by the platform on PUT #2** to the real gateway URL.74- `properties.group` is server-filled (`GenericProtocol` for `RemoteTool`).75- `properties.credentials` is scrubbed to `null` on GET.76- `properties.peRequirement` defaults to `"NotRequired"`.7778Allowed `authType` for `category=RemoteTool` (per `api-version=2025-04-01-preview`):79`None, CustomKeys, OAuth2, ProjectManagedIdentity, DeveloperConnection, UserEntraToken, AgentUserImpersonation, AgenticIdentityToken, AgenticUser, UserTokenAndProjectManagedIdentity`. `ApiKey` is **rejected** for `RemoteTool`. The authoritative list is whatever the [Cognitive Services projects API reference](https://learn.microsoft.com/rest/api/aiservices/) returns for the current API version β if you hit `invalid_payload: unsupported authType`, re-check against the schema for the version you're calling.8081## Decision tree8283| User scenario | `authType` | `metadata.type` | Notes |84|---|---|---|---|85| Catalog tile tagged "Custom Β· Preview" (Box, Pipedrive, GitHub, Salesforce, Outlook, β¦) | `OAuth2` | `gateway_connector` | **Connector-namespace managed MCP.** Powered by the Connector Namespace in your Foundry account; the namespace handles OAuth, token storage, and per-user passthrough. Needs **two** PUTs plus `listConsentLinks` per caller (see [Gateway connector full flow](#gateway-connector-full-flow)). |86| Catalog MCP tile with Microsoft-managed OAuth (no `client_id` needed) | `OAuth2` | `catalog_MCP` | Foundry brokers the OAuth app for you. The Catalog API tile **prepopulates** `target` (server URL); `listConsentLinks` flow same as gateway. |87| Catalog MCP tile with **your own** OAuth App | `OAuth2` | (omit) | Supply your own `client_id` + `client_secret` + raw `authorizationUrl` / `tokenUrl` / `scopes`. Do **not** mix BYO `credentials` with `metadata.type=catalog_MCP`. See [BYO OAuth caveats](#byo-oauth-app-against-a-catalog-mcp-server). |88| Remote MCP, Azure-side identity (project MI calls the server) | `ProjectManagedIdentity` | `catalog_MCP` *(when listed)* or `generic_mcp` | For catalog-listed MCP servers, prefer `catalog_MCP` so `target` is prepopulated. Requires `audience` in `metadata`. See [PMI limitations](#projectmanagedidentity-limitations). |89| Remote MCP, static shared secret / header key | `CustomKeys` | `catalog_MCP` *(when listed)* or `generic_mcp` | Header **name and format** are NOT always `Authorization: Bearer ...`. Read the required header name from the Catalog API entry's `x-ms-connection-parameters` and use that exact name in `credentials.keys`. |90| Remote MCP, user's Entra token forwarded | `UserEntraToken` | `generic_mcp` | Per-user identity passthrough. Not supported when the agent is published to Teams. Pair with `metadata.audience` for the upstream resource URI. |91| Custom OpenAPI / A2A tool (no MCP) | varies | n/a | Use the Custom subtab shapes; outside the MCP toolbox path. See [Custom subtab β OpenAPI / A2A](#custom-subtab--openapi--a2a). |9293## Catalog APIs β three backends, three calls9495There are **three** read endpoints the portal hits to populate a connection form. Programmatic callers should use the same three.9697### 1. Asset-gallery (Foundry's index)9899```100POST https://eastus.api.azureml.ms/asset-gallery/v1.0/tools101Headers:102Authorization: Bearer <token for https://management.azure.com>103Content-Type: application/json104Body:105{106"freeTextSearch": "*",107"filters": [108{ "field": "entityContainerId", "operator": "eq", "values": ["connectors-registry-prod-bl"] },109{ "field": "type", "operator": "eq", "values": ["tools"] },110{ "field": "annotations/name", "operator": "contains", "values": ["<name>"] }111],112"pageSize": 20113}114```115116- **Catalog lives only in `eastus`.** `westus2.api.azureml.ms` returns `totalCount=0` for the same body. `entityId`s are portable across project regions.117- Use this **only to discover the connector's `entityId`** β pull `objectId` out of the returned `entityId` (e.g. `β¦/objectId/github`). That `objectId` is the `connectorName` you pass to PUTs and the next two catalog calls.118- The response is a **thin index**. `properties.remotes[]`, `xMsSecuritySchemes`, OAuth endpoints, scopes, and operation schemas are **not** included. Direct `GET /asset-gallery/v1.0/tools/{entityId}` returns 404. There is no expand/projection flag that surfaces these fields β fetch them from calls 2 and 3 below.119120Two registries are indexed here β distinguished by `entityContainerId`:121122| Registry | `entityContainerId` | Contents | Pair with |123|---|---|---|---|124| Public catalog | `connectors-registry-prod-bl` | Catalog connector definitions (GitHub, Box, Salesforce, β¦). | `metadata.type=catalog_MCP` or `gateway_connector` |125| Private MCP entries | `registry-prod-bl` | MCP-server entries used by the portal Connections UI (e.g. `github-mcp-server`). Sometimes carries a canonical MCP URL when the public-catalog row lacks `remotes[]`. | `metadata.type=catalog_MCP` |126127Always query both when surfacing "available tools" to a user β the private MCP entries can fill gaps in the public catalog row.128129### 2. Logic Apps **managedApis** β OAuth source-of-truth130131```132GET https://management.azure.com/subscriptions/{sub}133/providers/Microsoft.Web/locations/{region}/managedApis/{connectorName}134?api-version=2016-06-01135```136137`connectorName` is the `objectId` from the asset-gallery `entityId`. Verified response shape for `github` (2026-05-21):138139```jsonc140{141"properties": {142"displayName": "GitHub",143"runtimeUrls": ["https://logic-apis-eastus.azure-apim.net/apim/github"],144"connectionParameters": {145"token": {146"type": "oauthSetting",147"oAuthSettings": {148"identityProvider": "GitHub",149"clientId": "faa5f56b825cbc649ae1", // Microsoft's default OAuth-App id150"scopes": ["repo","workflow","read:org","admin:org"],151"redirectMode": "Direct",152"redirectUrl": "https://logic-apis-eastus.consent.azure-apim.net/redirect"153}154}155}156}157}158```159160**Raw `authorizationUrl` / `tokenUrl` are NOT in this response.** Logic Apps abstracts them via the `identityProvider` string and resolves them inside the gateway. For BYO you must map `identityProvider β endpoints` yourself. Known mappings:161162| `identityProvider` | `authorizationUrl` | `tokenUrl` |163|---|---|---|164| `GitHub` | `https://github.com/login/oauth/authorize` | `https://github.com/login/oauth/access_token` |165| `Google` | `https://accounts.google.com/o/oauth2/v2/auth` | `https://oauth2.googleapis.com/token` |166| `Box` | `https://account.box.com/api/oauth2/authorize` | `https://api.box.com/oauth2/token` |167| `AzureActiveDirectory` / `aad3rdPartySNI` | `https://login.microsoftonline.com/common/oauth2/v2.0/authorize` | `https://login.microsoftonline.com/common/oauth2/v2.0/token` |168169For `identityProvider` values not in this table (`dynamicscrmonlinecertificate`, `salesforce`, `dropbox`, `oauth2generic`, β¦), look the provider's well-known OAuth endpoints up in its developer docs β the catalog API does not surface them.170171Use the `scopes` array from this response as the default scopes list. The catalog `clientId` is Microsoft's default OAuth App; replace it with your own only when going BYO.172173Derive `authType` from `connectionParameters`:174175- Any parameter with `type: oauthSetting` β `authType = OAuth2`.176- Else any parameter with `type: securestring` β `authType = CustomKeys`.177- Else β `authType = None` (anonymous) or `ProjectManagedIdentity` if the connector explicitly supports MI.178179### 3. Logic Apps **apiOperations** β operation catalog (`gateway_connector` only)180181For `gateway_connector` you need the list of operations the connector exposes plus each operation's parameter schema, because that's what gets serialized into `metadata.mcpserverConfigProperties` on PUT #2. Asset-gallery does not carry this.182183```184GET https://management.azure.com/subscriptions/{sub}185/providers/Microsoft.Web/locations/{region}/managedApis/{connectorName}186/apiOperations?api-version=2016-06-01187```188189Returns `value[]` of operations with `name`, `properties.summary` (display name), `properties.description`, `properties.annotation.family`, and `properties.visibility` (`important` / `advanced` / `internal`). Verified 2026-05-21: Box returns 14 operations including `ListRootFolder`, `ListFolder`, `GetFileMetadata`, `GetFileContent`, `DeleteFile`, `CreateFile`, plus several `On*` triggers (not agent-callable).190191To get parameter schemas, fetch a single operation with `$expand=properties/inputsDefinition`:192193```194GET .../managedApis/{connectorName}/apiOperations/{operationName}195?api-version=2016-06-01&$expand=properties/inputsDefinition196```197198`properties.inputsDefinition` is a JSON-Schema-shaped object with `type:"object"`, `properties:{...}`, and `required:[...]`. Map each entry to one `agentParameters` entry:199200| `inputsDefinition.properties[name]` field | β `agentParameters[].schema` field |201|---|---|202| `type` | `type` |203| `description` | `description` |204| `title` | `x-ms-summary` |205| `default` | `default` (omit if absent) |206207If `inputsDefinition.properties` is empty / missing, the operation takes no arguments and `agentParameters` is `[]` (e.g. Box `ListRootFolder`).208209Skip any operation whose `properties.isWebhook` or `isNotification` is `true` β these are Logic Apps triggers, not agent-callable actions.210211**Picking ops from a plain-language scenario.** Match the user's words against `properties.summary` and `properties.description`, then prefer the simplest variant (fewest required parameters) and the one whose `annotation.family` aligns with the user intent. For Box "list my files", `ListRootFolder` (zero params) wins over `ListFolder` (requires `id`); if the user asks to list a specific folder, register both.212213## Gateway connector full flow214215For Catalog tiles tagged `Custom Β· Preview` (Box, Pipedrive, GitHub, Salesforce, Outlook, iManage Work, PDF4me, Qdrant, Medallia, Fulcrum, monday.com, SuperMCP, IA-Connect JML, iMIS, Huddo Boards, The Events Calendar, PUG Gamified Engagement, Nitro Sign Enterprise Verified, Soft1, Elfsquad Product Configurator, MintNFT, β¦).216217### Step 1 β Discover218219Query the asset-gallery (call #1) for the connector. Extract:220221- `objectId` from `entityId` β `connectorName`222- Full `entityId` β `metadata.toolEntityId`223224Then call managedApis (call #2) and apiOperations (call #3) for OAuth and operation metadata.225226### Step 2 β PUT #1 (create connection)227228Verbatim PUT body (captured from the portal's Box wizard, 2026-05-21):229230```json231{232"properties": {233"authType": "OAuth2",234"category": "RemoteTool",235"target": "https://placeholder",236"credentials": {},237"connectorName": "box",238"metadata": {239"type": "gateway_connector",240"toolEntityId": "azureml://location/eastus/apiCenter/connectors-registry-prod-bl/type/tools/objectId/box/version/1",241"connectionproperties": "{\"connectorName\":\"box\"}"242}243}244}245```246247Spelling traps (case-sensitive):248249- `toolEntityId` β NOT `entityId`.250- `connectionproperties` β **lowercase**, value is a **stringified JSON object**, not a nested object. `"{\"connectorName\":\"box\"}"` is correct; `{"connectorName":"box"}` is rejected.251- `connectorName` appears at top-level under `properties` **and** inside `metadata` and inside `connectionproperties`.252253`target = "https://placeholder"` is the **persisted value on PUT #1**, not a stub. There is no follow-up call that rewrites it before PUT #2. Runtime dispatch keys off `metadata.toolEntityId` + `metadata.connectionproperties.connectorName` + OAuth consent state. PUT #2 (register-actions) rewrites `target` to the real gateway URL `https://app-XX.<region>.logic.azure.com/api/connectorGateways/{envId}/mcpServerConfigs/{connectionName}/mcp`.254255### Step 3 β Per-caller consent256257For every distinct end-user (or service principal), call `listConsentLinks`:258259```260POST .../connections/{name}/listConsentLinks?api-version=2025-04-01-preview261```262263Verbatim portal body:264265```json266{267"parameters": [{268"objectId": "<caller AAD oid>",269"parameterName": "token",270"redirectUrl": "https://ai.azure.com/nextgen/authConsentPopup",271"tenantId": "<caller AAD tid>"272}]273}274```275276Notes:277278- The portal sends `redirectUrl=https://int.ai.azure.com/...` from the INT environment; for production (`ai.azure.com`) use `https://ai.azure.com/nextgen/authConsentPopup`. The redirect URL only gates which Foundry origin the OAuth popup closes back into β it does not affect what tokens are minted.279- Returns a per-user OAuth authorization URL (e.g. a `box.com/api/oauth2/authorize?...` link). User navigates β consents β gateway stores the token.280- Cross-tenant calls return `InvalidConsentLinkParameter` (`objectId` + `tenantId` must match the caller principal).281282#### Consent link expiry (~1 hour)283284Each `listConsentLinks` response mints a short-lived signed token (β 1 hour TTL based on `ExpirationTime` in the base64 payload). A `500` from the consent host when clicking the link is most often caused by an **expired or stale link**, not a server outage. Fix: call `listConsentLinks` again to get a fresh link and use it immediately. Do not reuse a link from a previous step or previous session.285286#### Portal popup lifecycle (pending-true happy path)287288The portal pre-opens a blank popup (`about:blank`) before calling `listConsentLinks`, then drives the flow as follows once the consent URL is in hand. Code-first callers should replicate this:2892901. Register listeners on `window.postMessage` **and** `BroadcastChannel('connector-oauth-callback')` to receive completion signals.2912. Navigate the popup to the consent URL.2923. Poll `popup.closed` every 1 second to detect finish / dismiss.2934. When the popup closes, wait **500 ms** grace for any in-flight postMessage / BroadcastChannel messages.2945. If a `{ pending: true }` signal arrives (consent completed server-side but no authorization code returned to the opener):295- Issue a **PUT** to the connection (same body as the original create PUT) to prompt the backend to finalise auth state.296- If `overallStatus` is `Connected` in the response, done β .297- Otherwise **poll `GET .../connections/{name}`** every 2 seconds, up to **15 attempts**, until `overallStatus` flips to `Connected`.2986. If **no signal** before popup close, treat as user-cancelled and surface an error.2997. **Cleanup:** remove listeners, clear polling, force-close the popup if still open.300301The `{ pending: true }` path is the normal happy-path because the provider closes the popup by redirecting to `ai.azure.com/nextgen/authConsentPopup`, which has no JavaScript opener to post back to. **Don't assume consent is done just because the popup closed.** The "blank Foundry page" seen after authorising in a detached tab is this same redirect arriving without an opener β the gateway token is still stored; retry PUT #2 to confirm.302303#### Consent-host hosts304305Links served from `logic-apis-df.consent.azure-apim.net` are the **dogfood / INT** consent host (DF = dogfood). Production region traffic goes through `logic-apis-{region}.consent.azure-apim.net` (e.g. `logic-apis-eastus.consent.azure-apim.net`). Either host can return DF links depending on which Logic Apps environment the connector is deployed in; the caller cannot force the host.306307#### Dogfood OAuth-app runtime allowlist trap308309Some connectors (Spotify `spotifyip` confirmed) are backed by a **dogfood-env Microsoft OAuth app** registered in provider "development mode" with a hard-coded test-user allowlist. Consent + `Connected` status work fine code-first for any caller, but `tools/call` at runtime returns:310311```json312{ "error": { "code": 403, "source": "...logic-df.azure-apihub.net",313"innerError": "Check settings on https://developer.spotify.com/dashboard, the user may not be registered." } }314```315316Detect by inspecting the consent URL's first 302: if the `redirect_uri` is `https://global-test.consent.azure-apim.net/redirect` (rather than `global.consent...`), the connector is on the dogfood OAuth app. **The connection will still go Connected and `tools/list` will work**; only the actual API invocation fails. Not fixable client-side; requires Microsoft to promote the app or add the caller's email to the provider-side allowlist.317318```pwsh319$consentUrl = ($r.Content | ConvertFrom-Json).value[0].link320try { Invoke-WebRequest -Uri $consentUrl -MaximumRedirection 0 -ErrorAction Stop | Out-Null }321catch { $loc = $_.Exception.Response.Headers.Location.ToString() }322if ($loc -match 'global-test\.consent\.azure-apim\.net') {323Write-Warning "Connector uses dogfood OAuth app; tools/call may 403 with 'user may not be registered' even after Connected."324}325```326327### Step 4 β PUT #2 (register actions)328329After OAuth, the portal issues a **second PUT** against the **same connection name** to register which connector operations the agent can invoke. **Without this PUT the runtime has no actions to dispatch even though `overallStatus` shows `Authenticated`.**330331The body is identical to PUT #1 plus an additional `metadata.mcpserverConfigProperties` field (stringified JSON). Verbatim example for Box connection `box-5`:332333```jsonc334{335"properties": {336"authType": "OAuth2",337"category": "RemoteTool",338"target": "https://placeholder",339"credentials": {},340"connectorName": "box",341"metadata": {342"type": "gateway_connector",343"toolEntityId": "azureml://location/eastus/apiCenter/connectors-registry-prod-bl/type/tools/objectId/box/version/1",344"connectionproperties": "{\"connectorName\":\"box\"}",345"mcpserverConfigProperties": "{\"description\":\"\",\"state\":\"Enabled\",\"connectors\":[{\"name\":\"box\",\"connectionName\":\"box-5\",\"displayName\":\"box\",\"description\":\"\",\"operations\":[{\"name\":\"GetFileMetadata\",\"displayName\":\"Get file metadata using id\",\"description\":\"\",\"userParameters\":[],\"agentParameters\":[{\"name\":\"id\",\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the file in Box.\",\"x-ms-summary\":\"File Id\"}}]}]}]}"346}347}348}349```350351Decoded `mcpserverConfigProperties` schema:352353```jsonc354{355"description": "",356"state": "Enabled",357"connectors": [358{359"name": "<connectorName>", // same as properties.connectorName360"connectionName": "<this connection name>",361"displayName": "<connectorName>",362"description": "",363"operations": [364{365"name": "<OperationId>", // operation id from apiOperations366"displayName": "<friendly>",367"description": "",368"userParameters": [], // bound at connection time (rare for CustomΒ·Preview)369"agentParameters": [ // parameters the agent fills at call time370{371"name": "<paramName>",372"schema": {373"type": "string|number|boolean",374"description": "...",375"x-ms-summary": "...",376"default": "..." // optional377}378}379]380}381]382}383]384}385```386387Each operation in `operations[]` corresponds 1:1 to one `apiOperations` entry; `agentParameters[].schema` is translated from `inputsDefinition.properties` per the mapping in [Catalog APIs Β§3](#3-logic-apps-apioperations--operation-catalog-gateway_connector-only).388389The portal lets the user multi-select via checkboxes in the wizard's "Configure actions" page; the selection is serialized into this string. When the selection changes later, the portal **replaces `mcpserverConfigProperties` wholesale** β no merge. Your code must do the same: any time the agent-callable op list changes, re-run PUT #2 with the full new list.390391### Step 5 β `overallStatus` flip semantics392393Two independent conditions must BOTH be true for `overallStatus` to flip `Unauthenticated` β `Connected`:3943951. **PUT #2 issued with non-empty `metadata.mcpserverConfigProperties`** (rewrites `target` to the real gateway URL; target rewrite is visible immediately on PUT #2 regardless of consent state).3962. **OAuth consent completed** (user followed the `listConsentLinks` URL and clicked Authorize). Gateway then stores the token.397398Order-independent observations:399400- PUT #2 before consent β `target` rewrites, status stays `Unauthenticated`.401- Consent before PUT #2 β status stays `Unauthenticated` until PUT #2 fires; PUT #2 then flips to `Connected` in the same response.402403## Body shape β `OAuth2` + `catalog_MCP` (Microsoft-managed OAuth)404405Use when the catalog entry is an MCP server and you accept Microsoft's managed OAuth App + consent flow (no BYO secret):406407```json408{409"properties": {410"authType": "OAuth2",411"category": "RemoteTool",412"target": "https://api.githubcopilot.com/mcp",413"credentials": {},414"metadata": {415"type": "catalog_MCP",416"toolEntityId": "azureml://location/eastus/apiCenter/connectors-registry-prod-bl/type/tools/objectId/github/version/1"417},418"peRequirement": "NotRequired"419}420}421```422423For MCP URL discovery when `connectors-registry-prod-bl` lacks `remotes[]`, look up the peer entry in `registry-prod-bl` (e.g. `github-mcp-server`) β its asset-gallery row sometimes carries the canonical MCP URL. Consent uses the same `listConsentLinks` flow as gateway_connector.424425## BYO OAuth App against a catalog MCP server426427When the user has their own OAuth App (e.g. GitHub `https://github.com/organizations/<org>/settings/applications/<app-id>`) and wants the connection to mint tokens via *their* app instead of Microsoft's managed one. Verified shape, 2026-05-21:428429```json430{431"properties": {432"authType": "OAuth2",433"category": "RemoteTool",434"target": "<MCP server URL>",435"credentials": { "clientId": "<your client id>", "clientSecret": "<your client secret>" },436"authorizationUrl": "https://github.com/login/oauth/authorize",437"tokenUrl": "https://github.com/login/oauth/access_token",438"scopes": ["repo","workflow","read:org","admin:org"],439"peRequirement": "NotRequired"440}441}442```443444### Filling the OAuth fields from the catalog APIs4454461. **Find the connector `entityId`** β asset-gallery POST with `annotations/name contains <name>`. Pull `objectId` out of the returned `entityId`.4472. **Look up OAuth metadata** β `GET .../managedApis/<objectId>?api-version=2016-06-01`. From `properties.connectionParameters.token.oAuthSettings`:448- `identityProvider` β look up `authorizationUrl` / `tokenUrl` in the mapping table in [Catalog APIs Β§2](#2-logic-apps-managedapis--oauth-source-of-truth).449- `scopes` β use as the default scopes array (override only if the user explicitly needs different scopes).4503. **Supply your own `clientId` / `clientSecret`** in `credentials`. Do not reuse the catalog `clientId` from step 2 β that's Microsoft's managed OAuth App and you cannot mint with it.4514. **PUT** the body above.452453### Hard rules verified by probe (2026-05-21)454455- **`scopes` MUST be a JSON array.** A space-separated string returns `400 "Error when parsing request; unable to deserialize request body"`.456- **DO NOT send `useCustomConnector`.** It is ignored on input; server fills `false`.457- **DO NOT send `metadata.{type=catalog_MCP, toolEntityId, ...}`** for BYO. Those fields anchor the connection to the catalog's managed OAuth App and conflict with your supplied `credentials`.458- **DO NOT call `listConsentLinks`** for BYO β the gateway handles consent via the standard authorization_code flow using the server-filled `redirectUrl`. Calling `listConsentLinks` against a fresh BYO connection returns `404 AIGatewayConnectionNotFound`.459460### Server-filled response fields461462- `credentials` β `null` (scrubbed).463- `connectorName` β `<gatewayId>-<connectionName>` (your input ignored).464- `redirectUrl` β `https://global.consent.azure-apim.net/redirect/<32-hex>` β **the OAuth callback URL the provider (e.g. GitHub OAuth App) must allow-list**. Generated per-connection on first PUT. Two-pass flow:4651. PUT with placeholder client_secret.4662. Read `properties.redirectUrl` from the response.4673. Register it as the "Authorization callback URL" on the OAuth App.4684. PUT again with the real client_secret.469470### Caveat: `api.githubcopilot.com/mcp` rejects BYO OAuth-App tokens471472The GitHub Copilot MCP server requires GitHub-App-minted Copilot tokens (the `microsoft-foundry-agent-service` GitHub App). A token from a user OAuth App will be rejected at runtime even if the connection PUT is 200. For real BYO testing point `target` at a self-hosted GitHub MCP server, or use an OpenAPI tool against `api.github.com` instead.473474## `ProjectManagedIdentity` Remote MCP475476For MCP servers that accept Azure-side identity (the project's system MI calls the MCP server's bearer endpoint):477478```json479{480"properties": {481"authType": "ProjectManagedIdentity",482"category": "RemoteTool",483"target": "<MCP server URL with required query string>",484"metadata": { "type": "generic_mcp", "audience": "<upstream resource URI>" }485}486}487```488489For catalog-listed MCP servers, prefer `metadata.type = catalog_MCP` with `toolEntityId` so `target` is prepopulated. `audience` is **required for MI auth** β it tells Foundry which resource URI to request a token for. Read the required `audience` from the connector's catalog entry or its documentation (typical values: an app ID URI like `api://contoso-mcp`, or an Azure service resource ID like `https://cognitiveservices.azure.com`). If you omit `audience`, the MCP server rejects the call with 401.490491### `ProjectManagedIdentity` limitations492493Verified end-to-end against Azure Language `/language/mcp`, 2026-05-21:4944951. **Forwarder drops the query string.** The connection `target`'s `?api-version=...` is **not** preserved on the upstream call. If the upstream MCP requires a query parameter, PMI fails with 401/404 even when RBAC is correct.4962. **Forwarder mints the wrong audience.** The MI token Foundry sends does not have `aud=https://cognitiveservices.azure.com` or `https://ai.azure.com`. Setting `properties.audience` on the connection is accepted but **does not** change what is minted.4973. Endpoints not on the trust list reject the forwarded MI token with `-32007 PERMISSION_DENIED "Cannot pass Microsoft token to untrusted MCP endpoint"` (e.g. `api.githubcopilot.com/mcp`). This is the expected security gate.498499## `CustomKeys` Remote MCP500501Static header(s) injected on every upstream call. Minimum body:502503```json504{505"properties": {506"authType": "CustomKeys",507"category": "RemoteTool",508"target": "<MCP server URL>",509"credentials": { "keys": { "Ocp-Apim-Subscription-Key": "<value>" } },510"metadata": { "type": "generic_mcp" }511}512}513```514515Verified PUT 200 / GET 200 / DELETE 200 round-trip. The header name is **arbitrary** β it is forwarded as-is to the MCP server. Different connectors require different header shapes:516517- GitHub PAT: `Authorization: Bearer <pat>` or `Authorization: token <pat>` β catalog dictates.518- API-key services: `x-api-key: <key>` or `Ocp-Apim-Subscription-Key: <key>`.519- Multi-header schemes: e.g. `X-Account-Id: <id>` + `X-Account-Secret: <secret>`.520521Always read the canonical header set from the connector's `connectionParameters` (each `securestring` parameter names the header it maps to) before writing the `keys` block. **Do not default to `Authorization: Bearer`** β it's wrong for many connectors.522523For catalog-listed servers, swap `metadata.type` to `catalog_MCP` and add `toolEntityId`.524525## `UserEntraToken` Remote MCP526527For MCP servers that consume the *caller's* Entra token directly. Body includes `metadata.audience` so the platform mints the correct token for the upstream:528529```json530{531"properties": {532"authType": "UserEntraToken",533"category": "RemoteTool",534"target": "<MCP server URL>",535"metadata": { "type": "generic_mcp", "audience": "<upstream resource URI>" }536}537}538```539540Not available when the agent is published to Teams (Teams agents use the project MI).541542## Custom subtab β OpenAPI / A2A543544Not catalog-driven β the user provides the spec themselves. Each Save in this subtab maps to a single PUT against the same connections endpoint:545546| Tile | `authType` options | `target` | Notes |547|---|---|---|---|548| OpenAPI | `None`, `CustomKeys`, `ApiKey`, `OAuth2` | OpenAPI spec URL or upstream API base | Agent gets `tools[].openapi.auth.security_scheme.connection_id`. |549| A2A (Preview) | `None` / `CustomKeys` / `UserEntraToken` / `AAD` (mapped from UI) | A2A endpoint | `metadata.agentCardPath` default `/.well-known/agent-card.json`; agent gets `tools=[A2APreviewTool(project_connection_id=...)]`; runtime emits `a2a_preview_call` / `a2a_preview_call_output` events. |550| MCP | covered above | β | This tile is just a router to the catalog / BYO flows. |551552## Toolbox attach β `gateway_connector` tool naming553554Attach the same as `generic_mcp` β the tool block uses `type:"mcp"` and `project_connection_id` set to the **full ARM resource id** of the connection (NOT just the name):555556```jsonc557{558"tools": [{559"type": "mcp",560"server_label": "box5",561"project_connection_id":562"/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{acct}/projects/{proj}/connections/{connName}"563}]564}565```566567`tools/list` returns one MCP tool per registered operation. Tool names follow the verified pattern (probed 2026-05-22 against Box):568569```570<server_label>___<connectorName>_<OperationName>571```572573Note `___` (**three** underscores) between `server_label` and the rest, then a **single** `_` between `connectorName` and operation name. Example for Box attached with `server_label="box5"`:574575| `mcpserverConfigProperties` op | `tools/list` `name` | `description` |576|---|---|---|577| `ListRootFolder` | `box5___box_ListRootFolder` | `box - List files and folders in root folder` |578| `GetFileMetadata` | `box5___box_GetFileMetadata` | `box - Get file metadata using id` |579580The MCP tool's `inputSchema` is exactly the JSON schema derived from `apiOperations/{op}?$expand=properties/inputsDefinition` (the `agentParameters[].schema` values, re-keyed by parameter name). For an operation with no agent parameters, `inputSchema` is `{"type":"object"}`.581582Worked `tools/call` for "list my files in Box" β verified end-to-end:583584```jsonc585POST {dp}/toolboxes/{tb}/mcp?api-version=v1586{587"jsonrpc": "2.0", "id": 2, "method": "tools/call",588"params": { "name": "box5___box_ListRootFolder", "arguments": {} }589}590β 200591{592"jsonrpc": "2.0", "id": 2,593"result": {594"content": [{ "type": "text", "text": "[]" }],595"isError": false596}597}598```599600(`text` carries a JSON-stringified array of Box file/folder objects; empty `[]` means the root folder is empty.)601602### `outlook` connector β verified end-to-end (2026-05-22)603604Uses `identityProvider: oauth2generic` (MSA / consumers tenant). `connectorName = "outlook"`, `toolEntityId` objectId = `outlook`. `tools/call` response wraps in `{ "value": [...] }` (not a bare array like Box):605606```jsonc607POST {dp}/toolboxes/{tb}/mcp?api-version=v1608{609"jsonrpc": "2.0", "id": 2, "method": "tools/call",610"params": { "name": "outlook-1___outlook_GetEmailsV2",611"arguments": { "folderPath": "Inbox", "top": 3 } }612}613β 200614{615"jsonrpc": "2.0", "id": 2,616"result": {617"content": [{ "type": "text",618"text": "{\n \"value\": [\n { \"Subject\": \"...\", \"From\": \"...\", ... }\n ]\n}" }],619"isError": false620}621}622```623624Operations registered for the test: `GetEmailsV2` (read emails with `folderPath` / `top` / `fetchOnlyUnread` agent parameters) and `SendEmailV2` (send with `emailMessage` object param containing required `To`, `Subject`, `Body`). `SendEmailV2`'s top-level schema is `object` β pass it as a single nested `agentParameters` entry; the gateway flattens into the Logic Apps `emailMessage` envelope internally. The follow-up PUT after popup close (pending-true path) immediately returned `overallStatus: Connected` without needing the GET poll loop β outlook's MSA consent round-trips are fast.625626## Minimum attach + verify recipe627628Verifying a fresh connection is the only toolbox operation in scope of this reference. Toolboxes are upserted implicitly by `POST /versions`; no separate container create is needed.629630The `$dp` value below is the project's data-plane endpoint, in the same `{project_endpoint}` form used elsewhere in these references β `https://<account>.services.ai.azure.com/api/projects/<project>`. The host segment varies by Foundry account/region; read it from a non-`FOUNDRY_`-prefixed env var (see [toolbox-reference.md Β§ Agent env contract](toolbox-reference.md#agent-env-contract)) rather than hardcoding. The bearer-token resource is `https://ai.azure.com`, NOT ARM.631632```pwsh633# 0. Constants.634$dp = $env:PROJECT_ENDPOINT # https://<account>.services.ai.azure.com/api/projects/<project>635$tb = "default-tb"636$lbl = "box5" # becomes the "<label>___" prefix on tool names637$connId = "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<acct>/projects/<proj>/connections/<connName>"638$tok = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv639$hdr = @{ Authorization = "Bearer $tok"640"Content-Type" = "application/json"641"Foundry-Features" = "Toolboxes=V1Preview" # REQUIRED642Accept = "application/json, text/event-stream" }643644# 1. Create a toolbox version with the connection attached.645$body = @{ tools = @(@{646type = "mcp"647server_label = $lbl648project_connection_id = $connId649}) } | ConvertTo-Json -Depth 6 -Compress650$v = Invoke-WebRequest -Method POST -Headers $hdr -UseBasicParsing -Body $body `651-Uri "$dp/toolboxes/$tb/versions?api-version=v1"652$ver = ($v.Content | ConvertFrom-Json).version653654# 2. Promote the new version to default. default_version MUST be a JSON STRING, not a number.655# Use ${tb} to terminate the variable name unambiguously before the literal '?'.656Invoke-WebRequest -Method PATCH -Headers $hdr -UseBasicParsing `657-Body (@{ default_version = "$ver" } | ConvertTo-Json) `658-Uri "$dp/toolboxes/${tb}?api-version=v1" | Out-Null659660# 3. tools/list β expect one entry per registered op, named "<server_label>___<connectorName>_<OpName>".661$req = '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'662Invoke-WebRequest -Method POST -Headers $hdr -UseBasicParsing -Body $req `663-Uri "$dp/toolboxes/$tb/mcp?api-version=v1"664665# 4. tools/call β the prefixed name and arguments per inputSchema.666$call = @{ jsonrpc="2.0"; id=2; method="tools/call"; params=@{667name="$lbl`___box_ListRootFolder"; arguments=@{} } } | ConvertTo-Json -Depth 5 -Compress668Invoke-WebRequest -Method POST -Headers $hdr -UseBasicParsing -Body $call `669-Uri "$dp/toolboxes/$tb/mcp?api-version=v1"670```671672The `Foundry-Features: Toolboxes=V1Preview` header is mandatory β without it the dataplane returns 404. The response body for `/mcp` is plain JSON (no SSE `data:` framing) despite the `text/event-stream` Accept.673674## Required RBAC summary675676| Operation | Role |677|---|---|678| PUT any connection above | **Azure AI Developer** on the project (or **Cognitive Services Contributor** on the account) |679| Drive OAuth consent (`gateway_connector`, `catalog_MCP` managed-OAuth) | The end-user themselves, signed in to the subscription's tenant |680| `ProjectManagedIdentity` against a Cognitive Services upstream | Project MI needs the upstream's data-plane role (e.g. `Cognitive Services Language Owner` for `/language/mcp`) |681682## Pitfalls / common mistakes683684- **Do not forget PUT #2 for `gateway_connector`** ([Step 4](#step-4--put-2-register-actions)). The first PUT + OAuth flips status to `Authenticated` but the runtime has no actions to dispatch until you PUT again with `metadata.mcpserverConfigProperties`.685- **Do not invent a "real" target URL** for the `gateway_connector` flow on PUT #1. `"https://placeholder"` is correct on PUT #1; PUT #2 rewrites it.686- **Do not mix BYO `credentials` with `metadata.type=catalog_MCP`** in the BYO body. They conflict; the server accepts the PUT but the runtime uses the catalog's managed app and ignores your secret β or fails with consent confusion.687- **Do not send `scopes` as a space-separated string** anywhere. Always an array.688- **Do not call `listConsentLinks` for BYO OAuth.** Use only for `gateway_connector` and managed-OAuth `catalog_MCP`.689- **Do not assume the asset-gallery search response contains OAuth metadata** β it does not. Always pair it with the Logic Apps `managedApis` GET (or hardcode the identityProvider mapping) to get `scopes` and to derive `authorizationUrl` / `tokenUrl`.690- **Use exact field spelling** for `gateway_connector`: `toolEntityId` (NOT `entityId`), `connectionproperties` (lowercase, stringified JSON).691- **Sign in to the subscription's tenant** before calling `listConsentLinks` β it validates the caller principal owns the supplied `objectId` + `tenantId`.692- **Toolbox PATCH `default_version` must be a JSON STRING**, not a number. Sending `{"default_version": 1}` returns `400 invalid_payload "requires an element of type 'String', but the target element has type 'Number'"`. Use `{"default_version": "1"}`.693- **`metadata.audience` is required for `ProjectManagedIdentity`.** Without it the MCP server returns 401.694- **Header names for `CustomKeys` come from the catalog**, not from a default `Authorization: Bearer` template.695- **`ApiKey` is rejected** for `category=RemoteTool`. Use `CustomKeys` for static secrets.696- **OAuth consent is per-user, per-connection, per-project.** Each new caller hits `CONSENT_REQUIRED` (code `-32007`) once and must open the URL the toolbox returns.697- **`api.githubcopilot.com/mcp` rejects user OAuth-App tokens.** Use a self-hosted MCP or fall back to OpenAPI.698- **PMI forwarder drops `target` query strings and mints a fixed audience.** Setting `properties.audience` is accepted but does not change what is sent.699- **Network-secured Foundry** projects cannot use private-endpoint-only MCP servers β only public endpoints reachable from the Foundry data plane and the Connector Namespace.700701## References702703- [Tool Catalog](https://learn.microsoft.com/azure/foundry/agents/concepts/tool-catalog)704- [Toolbox (preview)](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/toolbox)705- [Private tools catalog](https://learn.microsoft.com/azure/foundry/agents/concepts/tool-catalog#private-tools-catalog)706- [Cognitive Services projects REST API](https://learn.microsoft.com/rest/api/aiservices/)707- [tool-mcp.md](tool-mcp.md) β prompt-agent MCP wiring (no toolbox)708- [toolbox-reference.md](toolbox-reference.md) β MCP endpoint, auth, testing, troubleshooting709- [agent-tools.md](agent-tools.md) β the agent-tools index710- [use-toolbox-in-hosted-agent.md](use-toolbox-in-hosted-agent.md) β wiring a toolbox into a hosted agent711