Day 6 of the Microsoft Foundry 100 Days / 100 Blogs series.
Every agent framework eventually runs into the same wall: you've got a model that reasons well, but the moment it needs to do something β read a GitHub issue, query an internal knowledge base, hit a partner API β you're back to writing bespoke client code, stuffing credentials into environment variables, and hoping nobody pastes a system prompt into a public repo. Multiply that by five agents, three environments, and a compliance team that wants an audit trail, and "add a tool" stops being a two-line change.
Model Context Protocol (MCP) was designed to solve exactly this: a standard wire format so any MCP-compatible client can talk to any MCP-compatible server without custom glue code. Microsoft Foundry Agent Service adopted MCP as a first-class tool type, but the more interesting engineering decision is what Foundry built on top of it β a construct called the Toolbox that turns MCP from "one more tool integration" into a governance layer for how agents get their hands on external capabilities.
This article is about the parts of that system a developer actually has to reason about: how MCP tool calls flow through the Responses API, what the six authentication models mean for your identity design, why the Toolbox exists and when it's worth the extra indirection, and where things break in production (timeouts, private networking, prompt injection through tool metadata).
MCP crossed from "interesting protocol" to "the thing everyone is standardizing on" faster than almost any AI infrastructure decision in the last two years. GitHub, Azure DevOps, Databricks Genie, Fabric, Neon, Vercel, and dozens of SaaS vendors now ship official MCP servers. If you're building agents on Foundry, the question isn't whether you'll connect to an MCP server β it's whether you'll do it in a way that's auditable, revocable, and doesn't leak a GitHub PAT into your agent instructions.
Foundry's answer is architecturally interesting because it separates three concerns that most tutorials conflate:
server_url, server_label). none to agentic-identity).
Understanding why those are three separate layers β instead of one config blob β is the actual engineering lesson here.
Model Context Protocol, published by Anthropic and now adopted widely across the industry (including Microsoft), defines a JSON-RPC-based contract between an MCP client (in our case, Foundry Agent Service) and an MCP server (GitHub, an internal REST wrapper, a data warehouse connector). The protocol standardizes three primitives:
tools/list and invoked via tools/call.
In practice, almost all production MCP usage today revolves around tools. What MCP gives you that a hand-rolled function-calling integration doesn't is discoverability β the client asks the server what it can do at connection time, rather than the tool schema being hardcoded into the client's source. That's what makes a single mcp tool declaration in Foundry capable of exposing dozens of GitHub operations without you writing a single wrapper function.
The trade-off is that discoverability cuts both ways: the server controls the tool descriptions the model sees, and the server can change its surface area at any time. That fact drives a lot of the security posture discussed later.
Foundry Agent Service implements MCP as a remote tool type, meaning the agent doesn't run an MCP client library itself β the platform's tool-execution layer does. When you declare an MCPTool on an agent, three things get wired together at the platform level:
ββββββββββββββββββββββ ββββββββββββββββββββββββββββ βββββββββββββββββββββββββ
β Foundry Agent β β Foundry Tool Execution β β Remote MCP Server β
β (Prompt or Hosted) ββββββββΆβ Layer (approval gate, ββββββββΆβ (GitHub, internal, β
β β β auth injection, retry) β β Toolbox endpoint) β
ββββββββββββββββββββββ ββββββββββββββββββββββββββββ βββββββββββββββββββββββββ
β² β
β βΌ
β project_connection_id
β (auth type resolved here)
ββββββββββββββββββββββββββββββββββ
[IMAGE: Professional architecture diagram showing a Foundry Agent (Prompt or Hosted) on the left connecting to a central "Foundry Toolbox (MCP-compatible endpoint)" box, which fans out to three MCP servers on the right β a GitHub MCP server (OAuth2), an internal MCP server behind a private VNet/Container Apps boundary, and a public Microsoft Learn MCP server (no auth). Annotate the arrows with "mcp_approval_request", "require_approval=always", and "project_connection_id" labels. Corporate blue/gray/white palette, clean documentation style.]
The important architectural point: the agent never sees raw credentials. The project_connection_id on the MCPTool declaration points at a Foundry project connection β a stored, RBAC-governed object that holds the auth configuration (API key, OAuth app registration, or an identity reference). At call time, the tool execution layer resolves the connection, attaches the right credential or token, makes the tools/call request to the MCP server, and returns the result back into the model's context window as a tool output.
This is the same separation of concerns you'd want in any multi-tenant system: the what (tool declaration) is agent-scoped, the how (credentials) is connection-scoped and centrally managed, and the where (network path) depends on whether the MCP server is public or sits behind a private endpoint.
Here's what actually happens on the wire when a Foundry agent with an MCP tool gets a user request that requires a tool call, assuming require_approval="always" (the recommended default):
get_me tool from the api-specs MCP server to answer "what's my GitHub username?"mcp_approval_request, containing the server label, tool name, and arguments. The response is mcp_approval_response (approve or deny) tied to the approval_request_id, referencing the previous response.id to keep continuity.
This matters because it means MCP tool calls are not fire-and-forget the way a local Python function tool might be. There's a full request/response round-trip for approval baked into the protocol surface, which is the mechanism Foundry uses to keep a human (or a policy engine) in the loop for anything that touches an external, third-party system.
If you set require_approval="never", this step is skipped entirely and the tool executes immediately β appropriate only for read-only, trusted, internal servers where the latency cost of a human-in-the-loop step isn't worth it.
This is the part of MCP integration that trips up most teams, because "authentication" for a remote tool call actually branches into six distinct patterns in Foundry, each suited to a different identity story:
| Auth type | Use when | What Foundry does |
|---|---|---|
none |
Public, unauthenticated MCP server (e.g., Microsoft Learn docs MCP) | No credential attached; request goes out as-is |
custom-keys |
Server needs a static header (PAT, API key) | Injects Header=Value pairs from the stored connection |
oauth2 |
Server supports OAuth2, either via a Foundry-managed connector or your own app registration | Handles the authorization code / token exchange, caches and refreshes tokens |
user-entra-token |
Passthrough of the calling user's Entra identity (e.g., Fabric, Power BI) | Exchanges the user's token for the target audience via On-Behalf-Of flow |
project-managed-identity |
Target resource trusts the Foundry project's system-assigned managed identity | Requests a token for the target audience using the project's MI |
agentic-identity |
Target resource should authorize the specific agent rather than the whole project | Requests a token scoped to the agent's own identity (ties into the Autopilot identity model from Day 3 of this series) |
The decision tree in practice:
oauth2 with --connector-name, and let Foundry manage the OAuth app registration entirely.project-managed-identity or custom-keys, but treat the connection object as a secret boundary β RBAC on who can create/read that connection matters as much as the key itself.
The agentic-identity vs project-managed-identity distinction is worth sitting with. If ten different agents in a project all call the same downstream resource under project-managed-identity, you get one identity in your access logs for all of them β fine for coarse-grained systems, insufficient if you need to answer "which agent did this" during an incident review. agentic-identity gives every agent its own principal, which is the same design tension covered in the Autopilot identity model piece earlier in this series β Foundry is consistent about pushing identity granularity down to the agent level wherever it can.
The simplest integration path uses a server-side prompt agent with an inline MCPTool. This is a simplified, illustrative example based on Foundry's Python SDK pattern β verify exact method names against your installed SDK version before running in production:
import json
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition, MCPTool
from openai.types.responses.response_input_param import McpApprovalResponse, ResponseInputParam
PROJECT_ENDPOINT = "https://<account>.services.ai.azure.com/api/projects/<project>"
MCP_CONNECTION_NAME = "my-mcp-connection" # project connection holding auth config
project = AIProjectClient(endpoint=PROJECT_ENDPOINT, credential=DefaultAzureCredential())
openai_client = project.get_openai_client()
mcp_tool = MCPTool(
server_label="api-specs",
server_url="https://api.githubcopilot.com/mcp",
require_approval="always",
project_connection_id=MCP_CONNECTION_NAME,
)
agent = project.agents.create_version(
agent_name="GitHubInsightsAgent",
definition=PromptAgentDefinition(
model="gpt-5-mini",
instructions="Use MCP tools as needed to answer GitHub-related questions.",
tools=[mcp_tool],
),
)
conversation = openai_client.conversations.create()
response = openai_client.responses.create(
conversation=conversation.id,
input="What is my username in my GitHub profile?",
extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}},
)
pending_inputs: ResponseInputParam = []
for item in response.output:
if item.type == "mcp_approval_request":
print(f"Server: {item.server_label} | Tool: {item.name} | Args: {json.dumps(item.arguments)}")
approved = input("Approve this MCP tool call? (y/N): ").strip().lower() == "y"
pending_inputs.append(
McpApprovalResponse(
type="mcp_approval_response",
approve=approved,
approval_request_id=item.id,
)
)
final = openai_client.responses.create(
input=pending_inputs,
previous_response_id=response.id,
extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}},
)
print(final.output_text)
project.agents.delete_version(agent_name=agent.name, agent_version=agent.version)
Three things worth internalizing from this snippet:
previous_response_id is how approvals chain back into the same reasoning turn.
For hosted agents built on Microsoft Agent Framework, the pattern shifts from an inline tool declaration to referencing a Toolbox endpoint β which is where things get more interesting architecturally:
import asyncio
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient, FoundryToolbox
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import MCPToolboxTool
from azure.identity import AzureCliCredential
PROJECT_ENDPOINT = "https://<account>.services.ai.azure.com/api/projects/<project>"
MCP_CONNECTION_NAME = "my-mcp-connection"
async def main() -> None:
credential = AzureCliCredential()
project = AIProjectClient(endpoint=PROJECT_ENDPOINT, credential=credential)
server_tool = MCPToolboxTool(
server_label="api-specs",
server_url="https://api.githubcopilot.com/mcp",
require_approval="always",
project_connection_id=MCP_CONNECTION_NAME,
)
toolbox = project.toolboxes.create_version(
name="mcp-server-toolbox",
description="Toolbox with the GitHub MCP server",
tools=[server_tool],
)
toolbox_mcp_url = (
f"{PROJECT_ENDPOINT}/toolboxes/{toolbox.name}"
f"/versions/{toolbox.version}/mcp?api-version=v1"
)
toolbox_tool = FoundryToolbox(credential, url=toolbox_mcp_url)
agent = Agent(
client=FoundryChatClient(credential=credential),
instructions="You are a helpful assistant that uses your MCP tool "
"to help with Microsoft documentation questions.",
tools=[toolbox_tool],
)
result = await agent.run("What is Microsoft Agent Framework?")
print(result.text)
if __name__ == "__main__":
asyncio.run(main())
The key shift: the hosted agent doesn't talk to GitHub's MCP server directly β it talks to your Toolbox's MCP endpoint, which in turn proxies to GitHub. That extra hop is the entire point, and it's worth understanding why.
A Foundry Toolbox bundles multiple tools β MCP servers, OpenAPI specs, Web Search, Code Interpreter, File Search, Azure AI Search, even Agent-to-Agent connections β behind a single MCP-compatible endpoint. Conceptually, it's an API gateway pattern applied to tool calling:
azd ai connection create my-mcp-conn \
--kind remote-tool \
--target https://api.githubcopilot.com/mcp/ \
--auth-type oauth2 \
--connector-name foundrygithubmcp
description: MCP server tools
connections:
- name: my-mcp-conn
azd ai toolbox create my-toolbox --from-file my-toolbox.yaml
Why does this indirection earn its keep instead of being unnecessary complexity?
allowed_tools, approval policy, and rate limiting can be enforced at the Toolbox layer rather than duplicated per-agent.
The OAuth consent flow surfaces a specific, easy-to-miss failure mode worth calling out explicitly: the first call from any new user through an OAuth-backed Toolbox connection returns a JSON-RPC error, not a tool result:
{
"error": {
"code": -32006,
"message": "User consent is required. Please visit: https://..."
}
}
This is expected behavior, not a bug β code your client to catch -32006, surface the consent URL, and retry after the user completes the OAuth flow in a browser. Treat it the same way you'd treat a 401 with a WWW-Authenticate challenge in a normal OAuth client.
Consider a developer-support agent that needs to: (1) look up internal ticket status from a private ticketing system, (2) search Microsoft Learn documentation, and (3) create GitHub issues on behalf of the user.
A Toolbox-first design looks like this:
auth-type none, require_approval="never" since it's read-only and Microsoft-operated.auth-type oauth2 with --connector-name foundrygithubmcp, require_approval="always" because issue creation is a write operation with real consequences.
All three get registered as connections and bundled into one support-agent-toolbox Toolbox. The agent code stays static β swapping the ticketing backend later, or adding a fourth MCP server, is a Toolbox YAML change, not an agent redeploy. This is the pattern that pays for the extra indirection: mixed trust levels, mixed auth models, and a hard requirement (write access to GitHub) that must never silently downgrade from "always approve."
Streaming and timeout behavior. MCP tool calls sit inside a synchronous request/response turn by default, which means a slow downstream MCP server directly extends your agent's response latency β and can trigger client-side timeouts if the tool takes longer than your HTTP client's patience allows. For genuinely long-running operations, Foundry's Toolbox MCP endpoint supports MCP tasks (preview), an extension to the spec for background-style execution β but your agent harness has to explicitly support MCP tasks to take advantage of it. Don't assume long-running tool support exists just because you're using a Toolbox; check the harness compatibility first.
Private networking. Public MCP endpoints work out of the box for both Basic and Standard agent setups, but internal MCP servers require a dedicated MCP subnet delegated to Microsoft.App/environments, with the server deployed on Azure Container Apps behind internal-only ingress. Foundry ships reference Bicep templates (19-private-network-agent-tools, 11-private-network-basic-project) for exactly this topology β worth starting from those rather than hand-rolling the VNet plumbing, since the MCP subnet delegation requirement is easy to get wrong on a first pass.
Version drift on third-party servers. Because MCP is discoverable, a server operator can change tool names, arguments, or descriptions at any time without notifying you. Pin allowed_tools explicitly rather than trusting "whatever the server currently exposes," and re-review the allow-list whenever you notice the server's behavior or exposed toolset has changed.
This is where MCP integration differs meaningfully from calling a REST API you control. You're feeding model context that originates from a third party β the tool descriptions, argument schemas, and even the results β directly into your agent's reasoning loop. That's a textbook indirect prompt injection surface: a malicious or compromised MCP server can craft a tool description or a tool result that instructs the model to take an unintended action on a later turn.
Concrete mitigations that map directly onto Foundry's controls:
From a scale perspective, the Toolbox pattern is the right default the moment you have more than one agent needing the same external capability: it turns an O(agents Γ servers) credential and configuration matrix into O(servers) connections plus O(agents) toolbox references. The cost angle is subtler β every MCP tool call in an approval-gated flow costs you an extra model turn (the turn that surfaces the mcp_approval_request and the turn that resumes after approval), which is real token and latency overhead compared to require_approval="never". That overhead is the price of the audit trail; don't remove it purely to save a few hundred tokens on a write-capable tool.
Connection reuse also matters for OAuth token lifecycle: a Toolbox-mediated connection handles token refresh centrally, so you're not paying the OAuth handshake cost (or risking expired-token failures) on every agent instance independently.
-32006 consent-required error
MCP isn't the only tool-integration mechanism in Foundry, and it isn't always the right one:
The trade-off in the other direction: MCP's dynamic discovery is precisely what makes it a bigger trust surface than a statically-defined OpenAPI tool. If you fully control both sides, a native tool or a pinned OpenAPI spec is simpler and has a smaller attack surface β reach for MCP when the "someone else's server, someone else's roadmap" dynamic is actually part of your requirement.
input() call is fine for a demo, not for a service with concurrent users.
MCP support in Microsoft Foundry isn't just "another tool type" β it's a deliberate architectural bet that tool integration should be governed the same way network access and identity are: centrally managed, RBAC-scoped, and auditable by default. The approval-gated call lifecycle, the six-way authentication decision tree, and the Toolbox-as-API-gateway pattern all point at the same underlying philosophy: treat every external MCP server as an untrusted dependency until you've explicitly decided otherwise, and design the plumbing so that decision is enforced at the platform layer rather than left to each agent's author to remember.
If you're building anything beyond a single-agent demo, start with a Toolbox from day one, keep require_approval="always" until you have real evidence a server deserves otherwise, and pick your auth type based on how granular your audit story needs to be β not just on what's fastest to wire up.
19-private-network-agent-tools, 11-private-network-basic-vnet)
(verify current SDK method signatures and preview-feature availability against the latest Foundry SDK release before shipping to production β MCP task support and some connector names are explicitly called out as preview/evolving in the docs)
This is Day 6 of the Microsoft Foundry 100 Days / 100 Blogs series β one deep technical post a day covering the breadth of the Foundry ecosystem. Previous days covered crash-resilient long-running agents, hosted agent protocols (Responses vs. Invocations), the Autopilot identity model, Foundry Local on-device inference, and the closed-loop Agent Optimizer.