Integrate
Connect and expose agents
Tracon supports three different external-agent directions. Keep them separate:
| Direction | Purpose | Registration | HTTP surface |
|---|---|---|---|
| MCP client | Bring remote tools, prompts, and resources into Tracon | UseMcp() |
Managed through /api/mcp-servers/* |
| MCP server | Publish a Tracon agent as an MCP tool | UseMcpServer() |
/tracon/mcp by default |
| A2A server | Publish an agent through the agent-to-agent protocol | UseA2A() |
/tracon/a2a/{agent} by default |
flowchart LR
accTitle: The three external-agent directions
accDescr: As an MCP client Tracon calls remote servers to gain tools. As an MCP server and as an A2A server Tracon is called by outside callers, and each of those directions has its own allowlist, budget, and credential requirement.
subgraph Inbound["Who can call your agents"]
MCPC["MCP caller"] --> MCPS["UseMcpServer<br/>allowlist · run budget · ExternalInvoke key"]
A2AC["A2A caller"] --> A2AS["UseA2A<br/>one agent card per exposed agent"]
end
MCPS --> AGENT["Your agent"]
A2AS --> AGENT
AGENT --> CLIENT["UseMcp<br/>discovery, refreshed on an interval"]
CLIENT --> REMOTE["Remote MCP server<br/>tools · prompts · resources"]
The first direction expands what your agents can call. The other two expand who can call your agents. They have different trust boundaries and must be enabled separately.
Consume a remote MCP server
Section titled “Consume a remote MCP server”Add the non-AOT Tracon.Mcp package and register discovery:
var tracon = builder.AddTracon() .UseOpenAI(builder.Configuration.GetSection(OpenAIProviderOptions.SectionName)) .UsePostgreSql(connectionString) .UseMcp(options => { options.RefreshInterval = TimeSpan.FromMinutes(5); options.ConnectionTimeout = TimeSpan.FromSeconds(30); options.MaxToolsPerServer = 100; });Create server records through the console or management API. Only the configuration key name is persisted; the credential value stays in your secret provider:
{ "endpoint": "https://mcp.example.com/mcp", "headerConfigurationKeys": { "Authorization": "Tracon:McpSecrets:ExampleToken", "X-Api-Key": "Tracon:McpSecrets:ExampleKey" }}dotnet user-secrets set "Tracon:McpSecrets:ExampleToken" "Bearer ..."dotnet user-secrets set "Tracon:McpSecrets:ExampleKey" "..."Each key name sits under Tracon:McpSecrets:, and a tenant other than the default one
uses Tracon:McpSecrets:<tenant>:. A credential in plain headers is refused with
400 — see credential headers.
The value is read on every connection, but a changed value does not reconnect by
itself: call the refresh endpoint below after rotating it.
Discovery is asynchronous and does not block startup. Refresh immediately after a
configuration change with POST {prefix}/api/mcp-servers/refresh. A server that
cannot be reached produces a warning; other servers continue. If the server was
listed before and its definition is unchanged, its last tool list is kept for
StaleToolRetention (15 minutes) after that listing, so one network blip does not
stop the agents that use it. After that the tools drop out, and an agent that names
one is refused until the server lists them again. A server that is edited, deleted
or disabled drops its tools at once.
An agent compiled against a tool also follows what the server says about it: when a refresh lists a changed description or input schema, the next run compiles again and the model sees the new definition.
The MCP client security boundary
Section titled “The MCP client security boundary”- Only HTTP and HTTPS transports are accepted. Tracon does not start MCP
stdioprocesses on the host. - Discovered tool names are
{server}_{tool}. A code-defined tool with the same name wins, so a remote server cannot replace it. - New MCP tools require approval by default.
- Discovery is tenant-scoped and limited to 100 tools per server by default.
- OAuth tokens live in memory. Plan for reauthorization after a process restart.
An agent can also receive static MCP resources at run start through
AgentDefinition.McpResourceUris. Each entry is {server}:{uri}. The limits are
64 KB per resource and 256 KB total. UseMcp() is required. This field is currently
code/HTTP-only; the console editor does not preserve it.
Publish agents as MCP tools
Section titled “Publish agents as MCP tools”Choose the allowlist before the application is built, then map the management API before the MCP endpoint:
var tracon = builder.AddTracon() .UseOpenAI(builder.Configuration.GetSection(OpenAIProviderOptions.SectionName)) .UsePostgreSql(connectionString) .UseMcpServer(options => { options.ExposedAgents.Add("support"); options.ToolNamePrefix = "acme"; options.Budget = new AgentRunBudget { MaxDepth = 1, MaxTotalRuns = 4, MaxTotalTokens = 40_000, }; });
var app = builder.Build();
app.MapTracon("/tracon", options =>{ options.AllowRemoteAccess = true; options.RequireRolePolicies = true;});app.MapTraconMcpServer();No agent is exposed by default. ExposeAllAgents exists, but an allowlist is safer
for a catalog that operators can edit. Each MCP call creates a fresh run budget; the
default maximum depth is one, so an external caller cannot open an unbounded agent
tree.
The endpoint inherits the loopback, bearer, and authorization settings from
MapTracon. Remote exposure also requires at least one active, unexpired API key
with the exact ExternalInvoke scope. A static bearer token alone is rejected at
startup. Create the key before enabling remote access.
An exposed agent cannot contain an approval-required tool. A tool needs approval when its
registration sets RequiresApproval, or when the function is already wrapped in
ApprovalRequiredAIFunction — the default of Microsoft Agent Framework’s
DockerShellExecutor.AsAIFunction(). The application waits
until the catalog is queryable, checks the selected agents, and prevents MCP requests
from running if an approval boundary would be crossed. An external protocol caller
cannot act as the missing human. A tool that gains approval after the agent was exposed
is refused at the call, and the run closes Failed with the error type
approval_unanswerable: nobody can ever answer it, so it does not wait.
The skill tools follow the same rule. They are not in the tool registry: Microsoft Agent
Framework adds them when it builds the agent, and it makes all three ask for approval. An
exposed agent that names skills must set Skills.DisableReadApproval on its definition
(see Approval of the skill tools). Without it,
the host refuses to open the agent and the message names the agent and the setting. With it,
load_skill and read_skill_resource run. run_skill_script keeps its approval, so the check does
not count it; a model that calls it ends the run with approval_unanswerable, unless a standing
approval rule covers the tool.
The file tools of a session workspace
are not in the registry either. The tools that change a file always ask for approval, so an exposed
agent may have a workspace only if it is read-only and asks for no approval to read:
Workspace.ReadOnly and Workspace.DisableReadApproval together. Any other workspace on an
exposed agent stops the host from opening it, and the message names the agent and the settings.
The check reads the catalog once, when the host starts, with no request in scope. An agent that you save through the API after that is not checked, and neither is an agent that a callable agent reaches. The run-time refusal is the boundary for those agents.
Long-running calls as MCP tasks
Section titled “Long-running calls as MCP tasks”By default, a tools/call for a published agent holds the connection open until the
agent finishes. A client that speaks the 2026-07-28 revision gets the response
headers together with the result, not before it, so a proxy that times out while it
waits for the first byte cuts a long call. Set EnableTasks to serve long-running calls
as MCP tasks instead: a task-aware client gets an immediate task id back, closes the connection, and polls
tasks/get for the result — useful when an agent’s response can take longer than a
client is willing to keep a connection open.
.UseMcpServer(options =>{ options.ExposedAgents.Add("support"); options.EnableTasks = true; options.TaskTimeToLive = TimeSpan.FromHours(2); options.TaskPollInterval = TimeSpan.FromSeconds(5);});EnableTasks is false by default: a client that never asks for the tasks
capability keeps getting today’s immediate response either way. The task id is the
same id the run itself uses, so a task also shows up as an ordinary run in the
management API and cost reports. An agent that ends up needing approval never
surfaces as a task waiting on input — it is reported as a completed task carrying
the same rejection message the synchronous path returns, for the same reason an
approval-required tool cannot be exposed in the first place: an external protocol
caller cannot act as the missing human.
Publish agents through A2A
Section titled “Publish agents through A2A”A2A exposes a distinct identity and agent card for each selected agent:
var tracon = builder.AddTracon() .UseOpenAI(builder.Configuration.GetSection(OpenAIProviderOptions.SectionName)) .UsePostgreSql(connectionString) .UseA2A(options => { options.ExposedAgents.Add("support"); options.Budget = new AgentRunBudget { MaxDepth = 1, MaxTotalTokens = 40_000, }; });
var app = builder.Build();
app.MapTracon("/tracon", options =>{ options.AllowRemoteAccess = true; options.RequireRolePolicies = true;});app.MapTraconA2A();The invocation URL is /tracon/a2a/support; its card is under
/tracon/a2a/support/.well-known/agent-card.json.
A2A names are frozen during service registration because the underlying hosting API registers one server per name. The agent implementation is still resolved from the catalog on every call, so updating a database definition changes later behavior, but adding a new name requires an application restart and registration change. There is no expose-all switch.
Tracon declares streaming, push notifications, and background A2A runs as
unsupported. The same ExternalInvoke, approval, tenant, and budget boundaries as
the MCP server apply.
A2A conversations
Section titled “A2A conversations”A message that carries a contextId continues a conversation. Tracon stores the
session of each conversation, so the agent sees the earlier turns of the same
contextId. A message without a contextId is a single turn: the response carries
a new contextId that the caller can send next.
A conversation belongs to the caller. Tracon stores it under the tenant, the agent,
the caller, and the contextId. A contextId is a handle, not a credential: a
caller that sends the contextId of another caller starts a new conversation and
cannot read or extend the first one.
| The request carries | The caller is |
|---|---|
| A signed-in user | That user. The user is read the way the audit trail reads its actor, so Tracon:Audit:ActorClaimType applies |
| An API key | That key |
The static AuthToken |
The installation |
| Nothing (loopback, no token) | The local operator. All such requests share one conversation space |
A signed-in user whose identity cannot be read is refused. Tracon does not move that user into the local operator’s space.
Keep these effects in mind:
- Cost. Each turn sends the whole history to the model, so the tokens per message
grow with the conversation. Send a new
contextIdto start over. - Storage. Every message Tracon answers leaves a session, also a message with no
contextId: Tracon makes one up and the reply carries it. Session retention is off by default because a session is user data. Set a maximum age for thesessionstarget to bound the table; see Retention defaults. - Parallel messages. Two messages on one
contextIdat the same time cannot both be stored. One succeeds. The other fails and the caller receives a JSON-RPC internal error (-32603). Send the messages one after the other. - Refusals. A refusal reaches the caller as the same opaque
-32603error. The cause is in the server log atErrorlevel. - Session ownership. When session ownership is on and
RequireAuthenticatedOwneristrue, a message whose identity Tracon cannot attribute is refused before the model is called, and so is a message that resumes a conversation another user owns. This matters when one API key speaks for several users: the key is one caller, the owner is the end user. BindIRunAttributionContextto the identity pipeline, or an A2A caller cannot open a conversation. - A turn that needs an approval. If a tool that needs approval reaches an exposed agent after startup, the turn ends with the same error and stays in the conversation; the call is recorded as declined, and the next message goes through.
- The console. A conversation is a session. The Sessions page lists it under the
exposed agent, with an identity that starts with
a2a:.
To store conversations somewhere else, register an AgentSessionStore keyed by the
agent name, or without a key; Tracon uses yours. To name the caller differently,
register an AgentIsolationKeyProvider. Both types come from Microsoft Agent
Framework and are marked experimental there. A store of your own receives the caller
as the isolation partition of the key and must keep the callers apart.
Both surfaces are configured in code, never from the console:
TraconMcpServerOptions for the MCP server and TraconA2AOptions for A2A.
They do not carry the same settings.
ExposeAllAgents and ToolNamePrefix are MCP-server settings only:
ExposeAllAgents opts out of naming published agents one by one, and
ToolNamePrefix keeps the published tool names from colliding with another
server’s. A2A has neither — its names are fixed at registration, which is why
there is no expose-all switch for it.
Budget, which bounds what an external caller may spend, is on both.
Production checklist
Section titled “Production checklist”- Expose only names whose input contract is safe for another system.
- Expect token use to grow with each turn of a
contextId; decide how a caller starts a new conversation. - Create a tenant-bound
ExternalInvokekey and test revocation and expiry. - Keep approval-required and high-impact tools out of exposed definitions.
- Set
Skills.DisableReadApprovalon every exposed agent that names skills, and keep script skills off exposed agents. - Give an exposed agent no session workspace, or a read-only one with
Workspace.DisableReadApproval. - Set token, depth, and child-run budgets for externally initiated work.
- Terminate HTTPS at a trusted proxy and preserve the request path.
- Alert on external run error rate, token use, latency, and rejected credentials.
- Test catalog availability when migrations are applied outside the process.
MCP server and A2A routes are protocol surfaces, not management endpoints. They do not appear in the generated OpenAPI operation count. Their runs still use the normal recording, tenancy, trace, cost, quota, and audit infrastructure.
Read next
Section titled “Read next”- Tools, skills, and MCP — the other direction: consuming an MCP server rather than publishing one
- Securing the endpoints — bind exposed agents to authentication, tenant scopes, and execution limits
- Compatibility matrices — which protocol revisions and transports are supported