MCP Server & Client
The Model Context Protocol is an open standard for how AI agents discover and invoke tools. This harness speaks MCP in both directions — it can expose its own tools to other agents (server) and discover tools hosted elsewhere (client). Same protocol, two roles.
What MCP is, in one minute
Before MCP, every AI integration was bespoke — your agent had to know about Slack's API, then Jira's, then GitHub's. MCP standardizes the conversation: servers describe their tools, prompts, and resources in a uniform JSON schema; clients discover and invoke them over a transport (HTTP or stdio). Once an integration speaks MCP, any MCP-aware agent can use it.
Tools — callable functions (with JSON schema for parameters). Prompts — parameterized prompt templates the client can fill in. Resources — addressable read-only content (files, query results, URIs). The harness exposes all three via its server and consumes Tools from external servers.
MCP supports two ways of physically connecting a client to a server:
HTTP transport — the server is a long-running web service at a URL.
The client makes HTTP requests to it. Use this for remote / shared servers.
Stdio transport — the server is a local program (often an npm
package or a Python script) that the client launches as a subprocess and
talks to via standard input / standard output. Use this for local-machine tools that
aren't worth running as a separate service.
Both look identical from the agent's perspective once tools are discovered. The
transport choice is just about where the server lives.
Two roles, two projects
| Role | Project | What it does |
|---|---|---|
| Server — expose this app's tools | Infrastructure.AI.MCPServer |
ASP.NET Core WebAPI host with MCP endpoints, JWT auth, rate limiting. |
| Client — consume external MCP servers | Infrastructure.AI.MCP |
Discovers tools at startup, wraps them as internal ITool, exposes to agents. |
The server side
Infrastructure.AI.MCPServer is hostable as a Presentation entry point — it's the
WebAPI process that lets external agents call into this harness. Setup is one line in
Program.cs:
builder.Services.AddCustomMCPServer(appConfig);
// ...
var app = builder.Build();
app.MapMcp()
.RequireAuthorization()
.RequireRateLimiting("RATE_LIMITER_AI_MCPSERVER_POLICY");
The AddCustomMCPServer extension chains:
.WithHttpTransport()— selects HTTP (vs stdio) for transport..LoadTools()— enumerates registeredIToolimplementations and converts their schemas viaIToolSchemaService..LoadPrompts()— registers prompt templates (defined as Razor or string-builder definitions)..LoadResources()— exposes resources, including meta-harness execution traces whenEnableMcpTraceResourcesis on.
Authentication
MCP server endpoints sit behind JWT Bearer authentication wired to your Entra ID tenant
(configurable via AppConfig.Azure.AzureADB2C). Validation parameters require
ValidateLifetime, ClockSkew = TimeSpan.Zero, and explicit issuer and
audience matching. Tokens that don't pass return 401 before any tool code runs.
Rate limiting
Every endpoint is rate-limited via the policy RATE_LIMITER_AI_MCPSERVER_POLICY.
The default is set in Infrastructure.APIAccess — tune via your DI registration if
your deployment requires different limits.
The client side
When this app boots, Infrastructure.AI.MCP reads
AppConfig.AI.McpServers.Servers and connects to each entry. For each server, it:
- Opens a connection over the configured transport (HTTP or stdio).
- Calls the MCP
tools/listmethod to enumerate exposed tools. - For each tool,
McpToolProvidercasts the SDK'sMcpClientToolstraight to aMicrosoft.Extensions.AIAITool(McpClientToolalready derives fromAITool). There is no internalMcpTool : IToolwrapper and no per-tool keyed DI registration. - Returns the resulting
AIToollist — per server, and merged across servers — so the agent can offer MCP tools alongside its built-in tools in the same turn.
"McpServers": {
"Servers": {
"bifrost": {
"Type": "Http",
"Url": "http://your-mcp-gateway:8090/mcp",
"Auth": { "Type": "Bearer", "BearerToken": "${BIFROST_TOKEN}" }
},
"local-fs": {
"Type": "Stdio",
"Command": "npx",
"Args": ["@modelcontextprotocol/server-filesystem", "/var/data"],
"StartupTimeoutSeconds": 30
}
}
}
Outbound authentication
Each server's Auth.Type controls how this app authenticates to that MCP
server. The supported values are None, ApiKey, Bearer
(the BearerToken shown above), and Entra. For Entra, the
harness mints its own short-lived, auto-rotating token via EntraTokenAuthHandler
(wired by McpConnectionManager) rather than forwarding a caller's credential:
"secure-remote": {
"Type": "Http",
"Url": "https://secure-mcp.example.com/mcp",
"Auth": {
"Type": "Entra",
"Scopes": ["api://<target-app-id>/.default"]
}
}
The secure-by-default Entra shape supplies only Scopes (at
least one is required) and optionally ClientId for a user-assigned
identity — no standing secret is stored. A client secret or certificate is supported as
an explicit fallback, but then both TenantId and
ClientId are required. A lone TenantId with no secret or
certificate is rejected as half-configured, so a forgotten credential
fails loudly instead of silently minting a token from an ambient identity.
Why this matters
Because MCP tools arrive as first-class AITool instances, the rest of the harness treats them like any other tool handed to the agent:
- They appear in the same tool list the model sees each turn.
- They can be referenced from a skill's
allowed-toolsdeclaration. - They emit the same OTel spans on invocation.
- They pass through the same tool-execution governance gates.
The agent has no idea whether a tool lives in this process or two networks away. That's the point.
Security scanning of MCP tools
Tools coming from external servers are not trusted by default. When
AppConfig.AI.Governance.EnableMcpSecurity is on, every tool's name,
description and parameter schema is scanned at discovery time — before the model ever
sees it — for tool poisoning, hidden instructions, description injection and homoglyph
typosquatting. That text is the attack surface: the harness copies it into the model's context
so the model knows the tool exists, which means a poisoned description works whether or not the
tool is ever called.
A finding at or above AppConfig.AI.Governance.McpToolBlockThreshold (default
High) causes the tool to be withheld — it is never published to the
model. Findings below that threshold are logged and counted, and the tool is still published.
Full detail, including how the detection rules were calibrated against live MCP servers, is in
the MCP tool-definition scanning
security guide.
A malicious MCP server could expose a tool described as "summarize text" but actually
exfiltrate prompts to an attacker. Only connect to MCP servers you trust, and keep
EnableMcpSecurity on. The deeper response-sanitization pipe
(ResponseSanitizationBehavior) also redacts known PII patterns from tool
responses before they reach the LLM.
The flip side: A2A
MCP is for tools. A2A (Agent-to-Agent) is the sister protocol for delegating
work between agents. Each agent publishes an Agent Card at
/.well-known/agent.json describing its capabilities and endpoint; an orchestrator
agent reads cards, picks the right delegate, and sends a task over HTTP. See
Infrastructure.AI/A2A/ for the host implementation. MCP and A2A are complementary
— MCP gives the agent more hands; A2A gives it more colleagues.
Try the MCP demo
The console app has an MCP tools discovery example — option [3] from the menu:
dotnet run --project src/Content/Presentation/Presentation.ConsoleUI -- --example mcp-tools
It connects to your configured MCP servers, lists every tool discovered, and prints their schemas. Run this when you change McpServers to verify wiring before letting an agent use them.