MCP Servers
MCP (Model Context Protocol) allows you to connect external tool servers to your agent. MCP servers run as separate processes and expose tools over a standardized protocol.
What is MCP?
MCP is a protocol for connecting AI agents to external tools. Instead of writing C# code for each integration, you can:
- Use existing MCP servers (filesystem, GitHub, databases, etc.)
- Connect to any MCP-compatible server
- Get automatic tool discovery
Agent ←──MCP Protocol──→ MCP Server (filesystem)
←──MCP Protocol──→ MCP Server (github)
←──MCP Protocol──→ MCP Server (database)Quick Start
1. Create a Manifest File
Create MCP.json:
{
"servers": [
{
"name": "filesystem",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-filesystem", "/workspace"],
"description": "File operations within workspace",
"enabled": true
}
]
}2. Register with AgentBuilder
var agent = await new AgentBuilder()
.WithMCP("./MCP.json")
.BuildAsync();That's it! The agent now has access to all tools from the filesystem MCP server.
Manifest Configuration
MCPServerConfig Properties
| Property | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique identifier for the server |
command | string | required | Command to start the server |
arguments | string[] | [] | Command arguments |
description | string | null | Description shown when collapsed |
enabled | bool | true | Whether to load this server |
enablecollapsing | bool | null | Group tools under a container |
requiresPermission | bool | true | Require user approval for tools |
functionResult | string | null | One-time message on expansion (appended to auto-generated) |
systemPrompt | string | null | Persistent instructions (injected into system prompt) |
timeout | int | 30000 | Connection timeout in ms |
retryAttempts | int | 3 | Number of retry attempts |
environment | object | null | Environment variables |
Full Example
{
"servers": [
{
"name": "filesystem",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-filesystem", "/workspace"],
"description": "File operations within workspace",
"enabled": true,
"enablecollapsing": true,
"requiresPermission": true,
"functionResult": "Working directory: /workspace",
"systemPrompt": "Never write outside /workspace. Always use absolute paths.",
"timeout": 30000,
"retryAttempts": 3,
"environment": {
"NODE_ENV": "production"
}
},
{
"name": "github",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-github"],
"description": "GitHub repository operations",
"enabled": true,
"enablecollapsing": true,
"systemPrompt": "Use search before listing all PRs. Always include PR descriptions.",
"environment": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}
]
}Registration Methods
From Manifest File
var agent = await new AgentBuilder()
.WithMCP("./MCP.json")
.BuildAsync();With Options
var agent = await new AgentBuilder()
.WithMCP("./MCP.json", options =>
{
options.FailOnServerError = false; // Continue if a server fails
options.ConnectionTimeout = TimeSpan.FromSeconds(30);
options.MaxConcurrentServers = 10;
})
.BuildAsync();From JSON String
var manifest = @"{
""servers"": [
{
""name"": ""filesystem"",
""command"": ""npx"",
""arguments"": [""-y"", ""@anthropic/mcp-filesystem""]
}
]
}";
var agent = await new AgentBuilder()
.WithMCPContent(manifest)
.BuildAsync();Collapsing
When enablecollapsing is true, all tools from a server are grouped under a container:
Before expansion: After expansion:
┌─────────────────────┐ ┌─────────────────────┐
│ MCP_filesystem │ ──► │ read_file │
│ (5 functions) │ │ write_file │
└─────────────────────┘ │ list_directory │
│ create_directory │
│ delete_file │
└─────────────────────┘Container Naming
Containers are named MCP_{serverName}:
filesystem→MCP_filesystemgithub→MCP_github
Enable Collapsing
In manifest:
{
"name": "filesystem",
"enablecollapsing": true,
"description": "File operations"
}The description appears in the container's tool definition.
Server Instructions
MCP servers support the same dual-context architecture as C# tools and Client tools:
| Parameter | Location | Lifetime | Use For |
|---|---|---|---|
functionResult | Conversation history | One-time on expansion | Additional context (appended to auto-generated message) |
systemPrompt | System prompt | Every turn while expanded | Critical rules, workflow |
** Requires Collapsing:**
functionResultandsystemPromptonly work whenenablecollapsing: true. If collapsing is disabled, instructions are ignored and validation will fail.
Important: The system automatically generates a base expansion message:
"{serverName} server expanded. Available functions: {FunctionList}"Your
functionResultis appended to this auto-generated message. Don't duplicate the expansion info—use it only for additional context. Passnullif the auto-generated message is sufficient.
In Manifest (Recommended)
{
"name": "filesystem",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-filesystem", "/workspace"],
"description": "File operations within workspace",
"enablecollapsing": true,
"functionResult": "Working directory: /workspace",
"systemPrompt": "Never write outside /workspace. Always use absolute paths."
}Via AgentConfig (Legacy)
You can also provide instructions via MCPServerInstructions in AgentConfig. These are injected as SystemPrompt (persistent):
var config = new AgentConfig
{
Collapsing = new CollapsingConfig
{
MCPServerInstructions = new Dictionary<string, string>
{
["filesystem"] = @"
FILESYSTEM RULES:
- Always use absolute paths
- Check if file exists before reading
- Never write outside /workspace"
}
}
};Note: Manifest-level
systemPromptandMCPServerInstructionsboth provide persistent instructions. Use the manifest approach for new projects.
Permissions
By default, MCP tools require user permission (requiresPermission: true). This is a safety feature since MCP servers can perform arbitrary operations.
Disable for Trusted Servers
{
"name": "readonly-docs",
"command": "npx",
"arguments": ["-y", "@example/docs-server"],
"requiresPermission": false
}Only disable for read-only or trusted servers.
Environment Variables
Pass environment variables to MCP servers:
{
"name": "github",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-github"],
"environment": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}",
"GITHUB_ORG": "my-org"
}
}Use ${VAR_NAME} syntax to reference system environment variables.
Common MCP Servers
| Server | Package | Description |
|---|---|---|
| Filesystem | @anthropic/mcp-filesystem | Read/write files |
| GitHub | @anthropic/mcp-github | Repository operations |
| PostgreSQL | @anthropic/mcp-postgres | Database queries |
| Brave Search | @anthropic/mcp-brave-search | Web search |
| Memory | @anthropic/mcp-memory | Persistent key-value store |
| Slack | @anthropic/mcp-slack | Messaging and channels |
| Google Drive | @anthropic/mcp-gdrive | Document access |
| Puppeteer | @anthropic/mcp-puppeteer | Browser automation |
Version Pinning
By default npx -y installs the latest version of a package on every startup. In production, pin to a specific version to prevent unexpected breaking changes:
{
"name": "filesystem",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-filesystem@1.2.3", "/workspace"]
}Or pre-install and reference directly:
{
"name": "filesystem",
"command": "node",
"arguments": ["./node_modules/@anthropic/mcp-filesystem/dist/index.js", "/workspace"]
}Recommendation: Pin versions in production (
@1.2.3), use latest (-ywithout version) in development.
Example: Multiple Servers
{
"servers": [
{
"name": "filesystem",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-filesystem@1.2.3", "/workspace"],
"enablecollapsing": true,
"systemPrompt": "Always use absolute paths. Never write outside /workspace."
},
{
"name": "github",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-github@0.6.2"],
"enablecollapsing": true,
"systemPrompt": "Use search before listing all PRs. Always include PR descriptions.",
"environment": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
},
{
"name": "search",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-brave-search@0.3.1"],
"enablecollapsing": false,
"environment": {
"BRAVE_API_KEY": "${BRAVE_API_KEY}"
}
}
]
}
---
## Authentication Patterns
MCP servers authenticate via environment variables passed in the manifest. Never hardcode credentials.
### API Key (most common)
```json
{
"name": "github",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-github"],
"environment": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}${VAR_NAME} is resolved from the host process's environment at startup.
OAuth Token
Some servers accept OAuth tokens the same way — just pass the token as an environment variable:
{
"name": "gdrive",
"command": "npx",
"arguments": ["-y", "@anthropic/mcp-gdrive"],
"environment": {
"GDRIVE_OAUTH_TOKEN": "${GDRIVE_OAUTH_TOKEN}"
}
}Multiple Credentials
{
"name": "myserver",
"command": "node",
"arguments": ["./myserver/index.js"],
"environment": {
"API_KEY": "${MY_API_KEY}",
"API_SECRET": "${MY_API_SECRET}",
"BASE_URL": "https://api.example.com"
}
}Rotating Credentials
For credentials that expire (e.g. short-lived tokens), register the MCP server via C# attribute instead of the manifest, so you can resolve the token at build time. ISecretResolver is injected automatically by the source generator — just declare it as a constructor parameter:
public partial class MyTools(ISecretResolver secrets)
{
[MCPServer]
public MCPServerConfig MyApi() => new()
{
Name = "myapi",
Command = "node",
Arguments = ["./myserver/index.js"],
Environment = new()
{
["API_TOKEN"] = secrets.Require("myapi:token")
}
};
}No DI container registration needed — the builder passes the configured resolver at startup.
Error Handling
Server Startup Failures
By default, if one server fails to start, the agent continues with others:
.WithMCP("./MCP.json", options =>
{
options.FailOnServerError = false; // Default
})Set FailOnServerError = true to fail fast if any server fails.
Timeouts
Configure connection timeout:
.WithMCP("./MCP.json", options =>
{
options.ConnectionTimeout = TimeSpan.FromSeconds(60);
})Or per-server in manifest:
{
"name": "slow-server",
"timeout": 60000
}Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Server not found | command not in PATH | Use full path or ensure npx is available |
| Tools not appearing | enabled: false | Set enabled: true |
| Permission denied | Server needs API key | Check environment variables |
| Timeout | Server slow to start | Increase timeout |
Debug: Check Loaded Tools
var agent = await new AgentBuilder()
.WithMCP("./MCP.json")
.BuildAsync();
// List all tools
foreach (var tool in agent.Tools)
{
Console.WriteLine($"{tool.Name}: {tool.Description}");
}C# Attribute Registration
In addition to JSON manifests, MCP servers can be registered directly in C# using the [MCPServer] attribute on a toolkit method that returns MCPServerConfig. This is useful when you need constructor injection (e.g. ISecretResolver) or want to keep the server definition co-located with related tools.
The class must be partial — the source generator produces the registration glue.
Basic Usage
public partial class MyTools
{
[MCPServer]
public MCPServerConfig FileSystem() => new()
{
Name = "filesystem",
Command = "npx",
Arguments = ["-y", "@anthropic/mcp-filesystem", "/workspace"],
Description = "File operations within workspace"
};
}From Manifest
Load config from MCP.json by server name. The method returns null — the runtime reads the manifest entry instead:
public partial class MyTools
{
[MCPServer("filesystem", FromManifest = "./MCP.json")]
public MCPServerConfig? FileSystem() => null;
}Nested Collapsing
When CollapseWithinToolkit = true, the MCP server's tools appear behind their own container nested inside the parent toolkit — two expansions required (toolkit first, then MCP server):
[Collapse("Research tools")]
public partial class ResearchTools
{
[MCPServer(CollapseWithinToolkit = true)]
public MCPServerConfig BraveSearch() => new()
{
Name = "brave",
Command = "npx",
Arguments = ["-y", "@anthropic/mcp-brave-search"],
SystemPrompt = "Always cite sources. Prefer authoritative references."
};
[AIFunction]
public string SummarizeResults(string content) { /* ... */ }
}With Permissions
public partial class MyTools
{
[MCPServer]
[RequiresPermission]
public MCPServerConfig GitServer() => new()
{
Name = "git",
Command = "git-mcp",
SystemPrompt = "Only operate on the current repository. Never force-push."
};
}Conditional MCP Servers
Use the generic form [MCPServer<TMetadata>] to show or hide the server based on runtime metadata:
public class SearchMetadata : IToolMetadata
{
public bool HasBraveKey { get; set; }
}
public partial class SearchTools(string apiKey)
{
[MCPServer<SearchMetadata>]
[ConditionalFunction("HasBraveKey")]
public MCPServerConfig BraveSearch() => new()
{
Name = "brave",
Command = "npx",
Arguments = ["-y", "@anthropic/mcp-brave-search"],
Environment = new() { ["BRAVE_API_KEY"] = apiKey }
};
}Register with metadata:
var agent = await new AgentBuilder()
.WithToolkit<SearchTools>(new SearchMetadata { HasBraveKey = true })
.BuildAsync();Attribute Reference
| Property | Type | Description |
|---|---|---|
ServerName | string? | Server name to look up when using FromManifest mode. Also settable via constructor: [MCPServer("name")]. |
Name | string? | Display name override (defaults to method name). |
Description | string? | Description override for the collapsed container. Auto-fetched from server's ServerInfo if not set. |
FromManifest | string? | Path to MCP.json to load server config from. Method should return null. |
CollapseWithinToolkit | bool | Group MCP tools behind their own nested MCP_* container (default: false). |
Best Practices
Use collapsing for servers with many tools to reduce context clutter.
Set requiresPermission: true for servers that can modify data.
Provide descriptions to help the agent understand what each server does.
Use environment variables for secrets—never hardcode tokens.
Set appropriate timeouts based on server startup time.
Use dual-context instructions: Put critical rules in
systemPrompt, additional context infunctionResult.Don't duplicate auto-generated messages in
functionResult—they waste tokens.
{
"name": "database",
"command": "npx",
"arguments": ["-y", "@example/mcp-postgres"],
"description": "Query the production database (read-only)",
"enablecollapsing": true,
"requiresPermission": true,
"functionResult": "Connected to: production (read-only)",
"systemPrompt": "LIMIT all queries to 1000 rows. Never use DELETE or UPDATE.",
"timeout": 10000,
"environment": {
"DATABASE_URL": "${DATABASE_URL}"
}
}