|
185 | 185 | " starts with `if skip(...): ...`.\n", |
186 | 186 | "- `mcp_token()` - a bearer token scoped to `https://ai.azure.com/.default`, the audience the\n", |
187 | 187 | " toolbox MCP endpoint expects.\n", |
188 | | - "- `TOOLBOX_HEADERS` - **every** call to a toolbox MCP endpoint must carry\n", |
189 | | - " `Foundry-Features: Toolboxes=V1Preview`. Forgetting it is the #1 cause of 404s.\n", |
| 188 | + "- `TOOLBOX_HEADERS` - the `Foundry-Features: Toolboxes=V1Preview` header. Needed **only** for consuming\n", |
| 189 | + " **skills** as MCP resources (section 10b) and the preview management REST API (section 5) - regular\n", |
| 190 | + " tool calls don't need it.\n", |
190 | 191 | "- `created_resources` - a tracker the cleanup section walks in reverse." |
191 | 192 | ] |
192 | 193 | }, |
|
242 | 243 | "# The MCP endpoint audience is ai.azure.com (NOT management.azure.com).\n", |
243 | 244 | "TOOLBOX_SCOPE = \"https://ai.azure.com/.default\"\n", |
244 | 245 | "\n", |
245 | | - "# Mandatory on every toolbox MCP request while the feature is in preview.\n", |
| 246 | + "# Only needed for skills MCP resources (10b) and the preview management REST API (5); not tool calls.\n", |
246 | 247 | "TOOLBOX_HEADERS = {\"Foundry-Features\": \"Toolboxes=V1Preview\"}\n", |
247 | 248 | "\n", |
248 | 249 | "credential = DefaultAzureCredential()\n", |
|
1765 | 1766 | "| **Developer** | `{project}/toolboxes/{name}/versions/{version}/mcp?api-version=v1` | one pinned version |\n", |
1766 | 1767 | "| **Consumer** | `{project}/toolboxes/{name}/mcp?api-version=v1` | the **default** version |\n", |
1767 | 1768 | "\n", |
1768 | | - "Both require the bearer token (scope `https://ai.azure.com/.default`) **and** the\n", |
1769 | | - "`Foundry-Features: Toolboxes=V1Preview` header on every request." |
| 1769 | + "Both require the bearer token (scope `https://ai.azure.com/.default`). The\n", |
| 1770 | + "`Foundry-Features: Toolboxes=V1Preview` header is only needed for skills-resource calls (section 10b), not for regular tool calls." |
1770 | 1771 | ] |
1771 | 1772 | }, |
1772 | 1773 | { |
|
1818 | 1819 | "claim (tool count *and* input-schema payload size). Then we run a `tool_search` -> `call_tool`\n", |
1819 | 1820 | "round-trip and read each tool's `_meta.tool_configuration` (which carries `require_approval`).\n", |
1820 | 1821 | "\n", |
1821 | | - "We use the `mcp` SDK's streamable-HTTP client, passing the bearer token and the mandatory\n", |
1822 | | - "preview header. If a tool's connection uses `oauth2`, the first call returns `CONSENT_REQUIRED`\n", |
| 1822 | + "We use the `mcp` SDK's streamable-HTTP client, passing the bearer token (no preview\n", |
| 1823 | + "header is needed for tool calls). If a tool's connection uses `oauth2`, the first call returns `CONSENT_REQUIRED`\n", |
1823 | 1824 | "(`-32006`) with a consent URL - we surface it so you can consent and retry." |
1824 | 1825 | ] |
1825 | 1826 | }, |
|
1844 | 1845 | "\n", |
1845 | 1846 | "async def _list_tools(url: str):\n", |
1846 | 1847 | " \"\"\"tools/list against a specific toolbox MCP endpoint.\"\"\"\n", |
1847 | | - " headers = {**TOOLBOX_HEADERS, \"Authorization\": f\"Bearer {mcp_token()}\"}\n", |
| 1848 | + " headers = {\"Authorization\": f\"Bearer {mcp_token()}\"}\n", |
1848 | 1849 | " async with streamablehttp_client(url, headers=headers) as (read, write, _):\n", |
1849 | 1850 | " async with ClientSession(read, write) as session:\n", |
1850 | 1851 | " await session.initialize()\n", |
|
1869 | 1870 | " assert \"tool_search\" in after_names, \"Tool Search not active - is the default version search-first?\"\n", |
1870 | 1871 | "\n", |
1871 | 1872 | " # Drive the meta-tool, then read approval config off a listed tool.\n", |
1872 | | - " headers = {**TOOLBOX_HEADERS, \"Authorization\": f\"Bearer {mcp_token()}\"}\n", |
| 1873 | + " headers = {\"Authorization\": f\"Bearer {mcp_token()}\"}\n", |
1873 | 1874 | " async with streamablehttp_client(CONSUMER_URL, headers=headers) as (read, write, _):\n", |
1874 | 1875 | " async with ClientSession(read, write) as session:\n", |
1875 | 1876 | " await session.initialize()\n", |
|
1958 | 1959 | "\n", |
1959 | 1960 | "The whole point of one governed endpoint is that *any* MCP client can use it unchanged. Here are\n", |
1960 | 1961 | "three: **Microsoft Agent Framework**, **LangGraph**, and the **Copilot SDK**. Each just needs the\n", |
1961 | | - "consumer URL, a bearer token, and the preview header." |
| 1962 | + "consumer URL and a bearer token." |
1962 | 1963 | ] |
1963 | 1964 | }, |
1964 | 1965 | { |
|
1985 | 1986 | "source": [ |
1986 | 1987 | "# --- Microsoft Agent Framework -------------------------------------------------\n", |
1987 | 1988 | "# MAF speaks MCP natively via MCPStreamableHTTPTool. Point it at the consumer URL.\n", |
1988 | | - "# Auth note: the toolbox MCP endpoint needs a bearer token + the preview header on\n", |
1989 | | - "# EVERY request, including the initialize handshake. MAF's header_provider only injects\n", |
| 1989 | + "# Auth note: the toolbox MCP endpoint needs a bearer token on every request, including the\n", |
| 1990 | + "# initialize handshake. MAF's header_provider only injects\n", |
1990 | 1991 | "# on tool *calls*, so we hand it a pre-authenticated http_client whose default headers\n", |
1991 | 1992 | "# cover connect + initialize. load_prompts=False skips a prompts/list the endpoint\n", |
1992 | 1993 | "# doesn't serve, and we build FoundryChatClient from the endpoint + credential (the\n", |
|
1997 | 1998 | "\n", |
1998 | 1999 | "async def run_maf():\n", |
1999 | 2000 | " http = httpx.AsyncClient(\n", |
2000 | | - " headers={**TOOLBOX_HEADERS, \"Authorization\": f\"Bearer {mcp_token()}\"},\n", |
| 2001 | + " headers={\"Authorization\": f\"Bearer {mcp_token()}\"},\n", |
2001 | 2002 | " follow_redirects=True,\n", |
2002 | 2003 | " timeout=httpx.Timeout(30.0, read=300.0),\n", |
2003 | 2004 | " )\n", |
|
2105 | 2106 | "source": [ |
2106 | 2107 | "# --- Copilot SDK ---------------------------------------------------------------\n", |
2107 | 2108 | "# The GitHub Copilot SDK (pip install github-copilot-sdk; Python 3.11+) consumes the toolbox as a\n", |
2108 | | - "# remote MCP server - same consumer URL, same bearer token + preview header. Remote MCP servers are\n", |
| 2109 | + "# remote MCP server - same consumer URL, same bearer token. Remote MCP servers are\n", |
2109 | 2110 | "# configured through create_session(..., mcp_servers={...}) with type \"http\".\n", |
2110 | 2111 | "# Docs: https://docs.github.com/copilot/how-tos/copilot-sdk/features/mcp\n", |
2111 | 2112 | "if not os.getenv(\"PROJECT_ENDPOINT\"):\n", |
|
2127 | 2128 | " \"foundry_toolbox\": {\n", |
2128 | 2129 | " \"type\": \"http\",\n", |
2129 | 2130 | " \"url\": CONSUMER_URL,\n", |
2130 | | - " \"headers\": {**TOOLBOX_HEADERS, \"Authorization\": f\"Bearer {mcp_token()}\"},\n", |
| 2131 | + " \"headers\": {\"Authorization\": f\"Bearer {mcp_token()}\"},\n", |
2131 | 2132 | " \"tools\": [\"*\"], # expose every toolbox tool (tool_search included)\n", |
2132 | 2133 | " },\n", |
2133 | 2134 | " },\n", |
|
2188 | 2189 | "# toolbox_tool = MCPStreamableHTTPTool(\n", |
2189 | 2190 | "# name=\"foundry_toolbox\",\n", |
2190 | 2191 | "# url=CONSUMER_URL, # same consumer URL as local\n", |
2191 | | - "# headers=TOOLBOX_HEADERS, # token injected by the runtime\n", |
2192 | 2192 | "# )\n", |
2193 | 2193 | "# return ChatAgent(\n", |
2194 | 2194 | "# chat_client=AzureAIAgentClient(project_client=project, model=MODEL_DEPLOYMENT),\n", |
|
0 commit comments