Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build and deploy AI applications on Azure AI Foundry using Microsoft's model catalog and AI services
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