MCP protocol types and interfaces for Model Context Protocol servers
$ dotnet add package COA.Mcp.ProtocolA comprehensive .NET framework for building and consuming Model Context Protocol (MCP) servers with built-in token optimization, AI-friendly responses, strong typing, and developer-first design.
Never used MCP before? Follow our 5-Minute Quickstart Guide 👈
Install: dotnet add package COA.Mcp.Framework
Copy this code into Program.cs:
using COA.Mcp.Framework.Server;
using COA.Mcp.Framework.Base;
using COA.Mcp.Framework.Models;
public class EchoTool : McpToolBase<EchoParams, EchoResult>
{
public override string Name => "echo";
public override string Description => "Echoes back your message";
protected override async Task<EchoResult> ExecuteInternalAsync(
EchoParams parameters, CancellationToken cancellationToken)
{
await Task.CompletedTask;
return new EchoResult
{
Success = true,
Response = $"You said: {parameters.Text}"
};
}
}
public class EchoParams { public string Text { get; set; } = ""; }
public class EchoResult : ToolResultBase
{
public override string Operation => "echo";
public string Response { get; set; } = "";
}
class Program
{
static async Task Main(string[] args)
{
// Easy start with optimized defaults
var builder = McpServerBuilder.CreateMinimal("My First MCP Server", "1.0.0");
builder.RegisterToolType<EchoTool>();
await builder.RunAsync();
}
}
dotnet run🎉 That's it! You have a working MCP server.
Want more examples?
Need help?
Ready for production? See Advanced Features below.
Transform your MCP server into an intelligent assistant that guides users toward optimal workflows:
var builder = new McpServerBuilder()
.WithServerInfo("My MCP Server", "1.0.0")
// 🆕 Load instructions from template files
.WithInstructionsFromTemplate("Templates/server-instructions.scriban", templateVariables)
// Advanced template-based instructions with built-in tool awareness
.WithTemplateInstructions(options =>
{
options.ContextName = "codesearch"; // Built-in: general, codesearch, database
options.EnableConditionalLogic = true;
options.CustomVariables["ProjectType"] = "C# Library";
})
// 🆕 Professional tool comparisons (no manipulation!)
.WithToolComparison(
task: "Find code patterns",
serverTool: "text_search",
builtInTool: "grep",
advantage: "Lucene-indexed with Tree-sitter parsing",
performanceMetric: "100x faster, searches millions of lines in <500ms"
)
// 🆕 Set enforcement level for recommendations
.WithWorkflowEnforcement(WorkflowEnforcement.Recommend) // Suggest/Recommend/StronglyUrge
// Tool priority and workflow suggestions
.ConfigureToolManagement(config =>
{
config.EnableWorkflowSuggestions = true;
config.EnableToolPriority = true;
config.UseDefaultDescriptionProvider = true; // 🆕 Imperative descriptions
})
// Smart error recovery
.WithAdvancedErrorRecovery(options =>
{
options.EnableRecoveryGuidance = true;
options.Tone = ErrorRecoveryTone.Professional;
});
Why This Matters:
🆕 Enhanced Tool Development:
// Tools can now specify priority and scenarios
public class MySearchTool : McpToolBase<SearchParams, SearchResult>, IPrioritizedTool
{
public override string Description =>
DefaultToolDescriptionProvider.TransformToImperative(
"Searches for code patterns with Tree-sitter parsing",
Priority);
// IPrioritizedTool implementation
public int Priority => 90; // High priority (1-100 scale)
public string[] PreferredScenarios => new[] { "code_exploration", "type_verification" };
}
Available Template Variables:
{{builtin_tools}} - Claude's built-in tools (Read, Grep, Bash, etc.){{tool_comparisons}} - Professional server vs built-in comparisons{{enforcement_level}} - Current workflow enforcement setting{{available_tools}} - Your server's available tools{{#has_builtin "grep"}} - Conditional logic for built-in tool detectionAdd intelligent type verification and TDD enforcement:
var builder = new McpServerBuilder()
.WithServerInfo("My MCP Server", "1.0.0")
.AddTypeVerificationMiddleware(options =>
{
options.Mode = TypeVerificationMode.Strict; // or Warning
options.WhitelistedTypes.Add("MyCustomType");
})
.AddTddEnforcementMiddleware(options =>
{
options.Mode = TddEnforcementMode.Warning; // or Strict
options.TestFilePatterns.Add("**/*Spec.cs"); // Custom test patterns
});
Benefits:
McpToolBase<TParams, TResult> ensures compile-time type safetyErrorInfo and RecoveryInfoValidateRequired(), ValidatePositive(), ValidateRange(), ValidateNotEmpty()CreateErrorResult(), CreateValidationErrorResult() with recovery stepsErrorMessages property for tool-specific guidanceIParameterValidator<TParams> eliminates casting with strongly-typed validationIResourceCache<TResource> supports any resource type with compile-time safetyBaseResponseBuilder<TInput, TResult> and AIOptimizedResponse<T> prevent object castingConfigureTokenBudgets() in server builderISimpleMiddleware for custom logicToolSpecificMiddleware for tool-specific hooksThe framework includes a strongly-typed C# client library for interacting with MCP servers:
// Create a typed client with fluent configuration
var client = await McpClientBuilder
.Create("http://localhost:5000")
.WithTimeout(TimeSpan.FromSeconds(30))
.WithRetry(maxAttempts: 3, delayMs: 1000)
.WithApiKey("your-api-key")
.BuildAndInitializeAsync();
// List available tools
var tools = await client.ListToolsAsync();
// Call a tool with type safety
var result = await client.CallToolAsync("weather", new { location = "Seattle" });
// Define your types
public class WeatherParams
{
public string Location { get; set; }
public string Units { get; set; } = "celsius";
}
public class WeatherResult : ToolResultBase
{
public override string Operation => "get_weather";
public double Temperature { get; set; }
public string Description { get; set; }
}
// Create a typed client
var typedClient = McpClientBuilder
.Create("http://localhost:5000")
.BuildTyped<WeatherParams, WeatherResult>();
// Call with full type safety
var weather = await typedClient.CallToolAsync("weather",
new WeatherParams { Location = "Seattle" });
if (weather.Success)
{
Console.WriteLine($"Temperature: {weather.Temperature}°");
}
Prompts provide interactive templates to guide users through complex operations:
// Define a prompt to help users generate code
public class CodeGeneratorPrompt : PromptBase
{
public override string Name => "code-generator";
public override string Description =>
"Generate code snippets based on requirements";
public override List<PromptArgument> Arguments => new()
{
new PromptArgument
{
Name = "language",
Description = "Programming language (csharp, python, js)",
Required = true
},
new PromptArgument
{
Name = "type",
Description = "Type of code (class, function, interface)",
Required = true
},
new PromptArgument
{
Name = "name",
Description = "Name of the component",
Required = true
}
};
public override async Task<GetPromptResult> RenderAsync(
Dictionary<string, object>? arguments = null,
CancellationToken cancellationToken = default)
{
var language = GetRequiredArgument<string>(arguments, "language");
var type = GetRequiredArgument<string>(arguments, "type");
var name = GetRequiredArgument<string>(arguments, "name");
return new GetPromptResult
{
Description = $"Generate {language} {type}: {name}",
Messages = new List<PromptMessage>
{
CreateSystemMessage($"You are an expert {language} developer."),
CreateUserMessage($"Generate a {type} named '{name}' in {language}."),
CreateAssistantMessage("I'll help you create that component...")
}
};
}
}
// Register prompts in your server
builder.RegisterPromptType<CodeGeneratorPrompt>();
Use the built-in variable substitution for dynamic templates:
var template = "Hello {{name}}, your project {{project}} is ready!";
var result = SubstituteVariables(template, new Dictionary<string, object>
{
["name"] = "Developer",
["project"] = "MCP Server"
});
// Result: "Hello Developer, your project MCP Server is ready!"
The framework provides a powerful middleware system for adding cross-cutting concerns to your tools:
public class MyTool : McpToolBase<MyParams, MyResult>
{
private readonly ILogger<MyTool>? _logger;
public MyTool(IServiceProvider? serviceProvider, ILogger<MyTool>? logger = null)
: base(serviceProvider, logger)
{
_logger = logger;
}
// Configure middleware for this specific tool
protected override IReadOnlyList<ISimpleMiddleware>? ToolSpecificMiddleware => new List<ISimpleMiddleware>
{
// Built-in token counting middleware
new TokenCountingSimpleMiddleware(),
// Custom timing middleware
new TimingMiddleware(_logger!)
};
// Your tool implementation...
}
public class TimingMiddleware : SimpleMiddlewareBase
{
private readonly ILogger _logger;
public TimingMiddleware(ILogger logger)
{
_logger = logger;
Order = 50; // Controls execution order
}
public override Task OnBeforeExecutionAsync(string toolName, object? parameters)
{
_logger.LogInformation("🚀 Starting {ToolName}", toolName);
return Task.CompletedTask;
}
public override Task OnAfterExecutionAsync(string toolName, object? parameters, object? result, long elapsedMs)
{
var performance = elapsedMs < 100 ? "⚡ Fast" : elapsedMs < 1000 ? "🚶 Normal" : "🐌 Slow";
_logger.LogInformation("✅ {ToolName} completed: {Performance} ({ElapsedMs}ms)",
toolName, performance, elapsedMs);
return Task.CompletedTask;
}
public override Task OnErrorAsync(string toolName, object? parameters, Exception exception, long elapsedMs)
{
_logger.LogWarning("💥 {ToolName} failed after {ElapsedMs}ms: {Error}",
toolName, elapsedMs, exception.Message);
return Task.CompletedTask;
}
}
📖 For complete documentation and advanced examples, see Lifecycle Hooks Guide
Check out our complete working example in examples/SimpleMcpServer/:
// From examples/SimpleMcpServer/Tools/CalculatorTool.cs
public class CalculatorTool : McpToolBase<CalculatorParameters, CalculatorResult>
{
public override string Name => "calculator";
public override string Description => "Performs basic arithmetic operations";
public override ToolCategory Category => ToolCategory.Utility;
protected override async Task<CalculatorResult> ExecuteInternalAsync(
CalculatorParameters parameters,
CancellationToken cancellationToken)
{
// Validate inputs using base class helpers
ValidateRequired(parameters.Operation, nameof(parameters.Operation));
ValidateRequired(parameters.A, nameof(parameters.A));
ValidateRequired(parameters.B, nameof(parameters.B));
var a = parameters.A!.Value;
var b = parameters.B!.Value;
double result = parameters.Operation.ToLower() switch
{
"add" or "+" => a + b,
"subtract" or "-" => a - b,
"multiply" or "*" => a * b,
"divide" or "/" => b != 0 ? a / b :
throw new DivideByZeroException("Cannot divide by zero"),
_ => throw new NotSupportedException($"Operation '{parameters.Operation}' is not supported")
};
return new CalculatorResult
{
Success = true,
Operation = parameters.Operation,
Expression = $"{a} {parameters.Operation} {b}",
Result = result,
Meta = new ToolMetadata
{
ExecutionTime = $"{stopwatch.ElapsedMilliseconds}ms"
}
};
}
}
// Program.cs for your MCP server
var builder = new McpServerBuilder()
.WithServerInfo("My MCP Server", "1.0.0")
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Information);
});
// Register your services
builder.Services.AddSingleton<IMyService, MyService>();
// Register your tools (two options)
// Option 1: Manual registration (recommended for explicit control)
builder.RegisterToolType<MyFirstTool>();
builder.RegisterToolType<MySecondTool>();
builder.RegisterToolType<LifecycleExampleTool>(); // Example with middleware
// Option 2: Automatic discovery (scans assembly for tools)
builder.DiscoverTools(typeof(Program).Assembly);
// Build and run
await builder.RunAsync();
The framework supports multiple transport types for flexibility:
// Default: Standard I/O (for Claude Desktop and CLI tools)
var builder = new McpServerBuilder()
.WithServerInfo("My Server", "1.0.0");
// Uses stdio transport by default
// HTTP Transport (for web-based clients)
var builder = new McpServerBuilder()
.WithServerInfo("My Server", "1.0.0")
.UseHttpTransport(options =>
{
options.Port = 5000;
options.EnableWebSocket = true;
options.EnableCors = true;
options.Authentication = AuthenticationType.ApiKey;
options.ApiKey = "your-api-key";
});
// WebSocket Transport (for real-time communication)
var builder = new McpServerBuilder()
.WithServerInfo("My Server", "1.0.0")
.UseWebSocketTransport(options =>
{
options.Port = 8080;
options.Host = "localhost";
options.UseHttps = false;
});
The framework provides enhanced configuration methods that give you access to the dependency injection container, eliminating the need for anti-patterns like manual BuildServiceProvider() calls:
// Register your dependencies first
var builder = new McpServerBuilder()
.WithServerInfo("My Server", "1.0.0");
builder.Services.AddSingleton<IDataService, DataService>();
builder.Services.AddScoped<IRepository, DatabaseRepository>();
// Enhanced configuration with service provider access
builder
.ConfigureTools((registry, serviceProvider) =>
{
// ✅ Clean: Access services without BuildServiceProvider()
var dataService = serviceProvider.GetRequiredService<IDataService>();
var customTool = new CustomTool(dataService);
registry.RegisterTool<CustomParams, CustomResult>(customTool);
})
.ConfigureResources((registry, serviceProvider) =>
{
// ✅ Access repository for dynamic resource registration
var repository = serviceProvider.GetRequiredService<IRepository>();
var provider = new DatabaseResourceProvider(repository);
registry.RegisterProvider(provider);
})
.ConfigurePrompts((registry, serviceProvider) =>
{
// ✅ Configure prompts with access to services
var dataService = serviceProvider.GetRequiredService<IDataService>();
var dynamicPrompt = new DataDrivenPrompt(dataService);
registry.RegisterPrompt(dynamicPrompt);
});
If you have existing code that manually calls BuildServiceProvider(), you can migrate to the enhanced API:
// ❌ Before: Anti-pattern with duplicate singletons
builder.ConfigureResources(registry =>
{
#pragma warning disable ASP0000
var serviceProvider = builder.Services.BuildServiceProvider();
var provider = serviceProvider.GetRequiredService<MyResourceProvider>();
registry.RegisterProvider(provider);
#pragma warning restore ASP0000
});
// ✅ After: Clean with proper dependency injection
builder.ConfigureResources((registry, serviceProvider) =>
{
var provider = serviceProvider.GetRequiredService<MyResourceProvider>();
registry.RegisterProvider(provider);
});
The enhanced API provides:
The framework provides granular control over logging to reduce noise and improve debugging experience:
var builder = new McpServerBuilder()
.WithServerInfo("My Server", "1.0.0")
.ConfigureLogging(logging =>
{
// Standard logging configuration
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Information);
// Optional: Configure specific categories
logging.AddFilter("COA.Mcp.Framework", LogLevel.Warning); // Quiet framework
logging.AddFilter("MyApp", LogLevel.Debug); // Verbose for your code
})
.ConfigureFramework(options =>
{
// Framework-specific logging options
options.FrameworkLogLevel = LogLevel.Warning; // Default framework log level
options.EnableDetailedToolLogging = false; // Reduce tool execution noise
options.EnableDetailedMiddlewareLogging = false; // Reduce middleware noise
options.EnableDetailedTransportLogging = false; // Reduce transport noise
// Advanced options
options.EnableFrameworkLogging = true; // Enable/disable framework logging entirely
options.ConfigureLoggingIfNotConfigured = true; // Don't override existing logging config
options.SuppressStartupLogs = false; // Show/hide startup messages
});
The framework uses these logging categories for fine-grained control:
COA.Mcp.Framework.Pipeline.Middleware - Middleware operations (type verification, TDD enforcement, etc.)COA.Mcp.Framework.Transport - Transport layer operations (HTTP, WebSocket, stdio)COA.Mcp.Framework.Base - Tool execution and lifecycle eventsCOA.Mcp.Framework.Server - Server startup and managementCOA.Mcp.Framework.Pipeline - Request/response pipeline processing// Minimal logging (production)
builder.ConfigureFramework(options =>
{
options.FrameworkLogLevel = LogLevel.Error;
options.EnableDetailedToolLogging = false;
options.EnableDetailedMiddlewareLogging = false;
options.EnableDetailedTransportLogging = false;
});
// Debug mode (development)
builder.ConfigureFramework(options =>
{
options.FrameworkLogLevel = LogLevel.Debug;
options.EnableDetailedToolLogging = true;
options.EnableDetailedMiddlewareLogging = true;
options.EnableDetailedTransportLogging = true;
});
// Completely disable framework logging
builder.ConfigureFramework(options =>
{
options.EnableFrameworkLogging = false;
});
The framework now supports automatic service startup, enabling dual-mode architectures where an MCP server can act as both a STDIO client and an HTTP service provider:
// Configure auto-started services alongside your MCP server
var builder = new McpServerBuilder()
.WithServerInfo("My MCP Server", "1.0.0")
.UseStdioTransport() // Primary transport for Claude
.UseAutoService(config =>
{
config.ServiceId = "my-http-api";
config.ExecutablePath = Assembly.GetExecutingAssembly().Location;
config.Arguments = new[] { "--mode", "http", "--port", "5100" };
config.Port = 5100;
config.HealthEndpoint = "http://localhost:5100/health";
config.AutoRestart = true;
config.MaxRestartAttempts = 3;
});
await builder.RunAsync();
public class ServiceConfiguration
{
public string ServiceId { get; set; } // Unique service identifier
public string ExecutablePath { get; set; } // Path to executable
public string[] Arguments { get; set; } // Command-line arguments
public int Port { get; set; } // Service port
public string HealthEndpoint { get; set; } // Health check URL
public int StartupTimeoutSeconds { get; set; } // Startup timeout (default: 30)
public int HealthCheckIntervalSeconds { get; set; } // Health check interval (default: 60)
public bool AutoRestart { get; set; } // Enable auto-restart (default: true)
public int MaxRestartAttempts { get; set; } // Max restart attempts (default: 3)
public Dictionary<string, string> EnvironmentVariables { get; set; } // Environment vars
}
var builder = new McpServerBuilder()
.WithServerInfo("Multi-Service MCP", "1.0.0")
.UseStdioTransport()
.UseAutoServices(
config =>
{
config.ServiceId = "api-service";
config.ExecutablePath = "api.exe";
config.Port = 5100;
config.HealthEndpoint = "http://localhost:5100/health";
},
config =>
{
config.ServiceId = "worker-service";
config.ExecutablePath = "worker.exe";
config.Port = 5200;
config.HealthEndpoint = "http://localhost:5200/health";
}
);
// Register a custom health check for advanced scenarios
// Use the enhanced configuration API with service provider access
builder.ConfigureResources((registry, serviceProvider) =>
{
var serviceManager = serviceProvider.GetRequiredService<IServiceManager>();
serviceManager.RegisterHealthCheck("my-service", async () =>
{
// Custom health check logic
var client = new HttpClient();
var response = await client.GetAsync("http://localhost:5100/custom-health");
return response.IsSuccessStatusCode;
});
});
This feature is ideal for:
The framework provides generic versions of key interfaces to eliminate object casting and improve type safety:
// Before: Non-generic parameter validation (still supported)
public class LegacyTool : McpToolBase<MyParams, MyResult>
{
protected override Task<MyResult> ExecuteInternalAsync(
MyParams parameters, CancellationToken cancellationToken)
{
// Parameters already validated and strongly typed!
return ProcessParameters(parameters); // No casting needed
}
}
// New: Explicit generic parameter validator for advanced scenarios
public class CustomValidationTool : McpToolBase<ComplexParams, ComplexResult>
{
private readonly IParameterValidator<ComplexParams> _validator;
public CustomValidationTool(IParameterValidator<ComplexParams> validator)
{
_validator = validator; // Strongly typed, no object casting
}
protected override async Task<ComplexResult> ExecuteInternalAsync(
ComplexParams parameters, CancellationToken cancellationToken)
{
// Custom validation with no object casting
var validationResult = _validator.Validate(parameters);
if (!validationResult.IsValid)
{
return CreateErrorResult("VALIDATION_FAILED",
string.Join(", ", validationResult.Errors.Select(e => e.Message)));
}
// Process strongly-typed parameters
return ProcessComplexParameters(parameters);
}
}
// Cache any resource type with compile-time safety
public class SearchResultResourceProvider : IResourceProvider
{
private readonly IResourceCache<SearchResultData> _cache; // Strongly typed cache!
private readonly ISearchService _searchService;
public SearchResultResourceProvider(
IResourceCache<SearchResultData> cache, // No more object casting
ISearchService searchService)
{
_cache = cache;
_searchService = searchService;
}
public async Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken ct)
{
// Check cache first - strongly typed, no casting
var cached = await _cache.GetAsync(uri);
if (cached != null)
{
return CreateReadResourceResult(cached); // Type-safe operations
}
// Generate new data
var searchData = await _searchService.SearchAsync(ExtractQuery(uri));
// Store in cache - type-safe storage
await _cache.SetAsync(uri, searchData, TimeSpan.FromMinutes(10));
return CreateReadResourceResult(searchData);
}
private ReadResourceResult CreateReadResourceResult(SearchResultData data)
{
return new ReadResourceResult
{
Contents = new List<ResourceContent>
{
new ResourceContent
{
Uri = data.OriginalQuery,
Text = JsonSerializer.Serialize(data), // Type-safe serialization
MimeType = "application/json"
}
}
};
}
}
// Register the strongly-typed cache
builder.Services.AddSingleton<IResourceCache<SearchResultData>, InMemoryResourceCache<SearchResultData>>();
// Before: Object-based response building (still supported for backward compatibility)
public class LegacyResponseBuilder : BaseResponseBuilder
{
public override async Task<object> BuildResponseAsync(object data, ResponseContext context)
{
return new AIOptimizedResponse // Returns object, requires casting
{
Data = new AIResponseData
{
Results = data, // object type, requires casting later
Meta = new AIResponseMeta { /* ... */ }
}
};
}
}
// New: Strongly-typed response building
public class TypedResponseBuilder : BaseResponseBuilder<SearchData, SearchResult>
{
public override async Task<SearchResult> BuildResponseAsync(SearchData data, ResponseContext context)
{
return new SearchResult // Strongly typed return, no casting needed
{
Success = true,
Operation = "search_data",
Query = data.Query,
Results = data.Items, // Type-safe property access
TotalFound = data.Items.Count,
ExecutionTime = context.ElapsedTime,
// No object casting anywhere!
};
}
}
// Using the generic AIOptimizedResponse<T>
public class OptimizedSearchTool : McpToolBase<SearchParams, AIOptimizedResponse<SearchResultSummary>>
{
protected override async Task<AIOptimizedResponse<SearchResultSummary>> ExecuteInternalAsync(
SearchParams parameters, CancellationToken cancellationToken)
{
var searchData = await SearchAsync(parameters.Query);
return new AIOptimizedResponse<SearchResultSummary> // Generic type, no casting!
{
Success = true,
Operation = "search_optimized",
Data = new AIResponseData<SearchResultSummary>
{
Results = new SearchResultSummary
{
Query = parameters.Query,
TotalMatches = searchData.Count,
TopResults = searchData.Take(5).ToList()
},
Meta = new AIResponseMeta
{
TokenUsage = EstimateTokens(searchData),
OptimizationApplied = searchData.Count > 100,
ResourceUri = searchData.Count > 100 ? StoreAsResource(searchData) : null
}
}
};
}
}
// Easy migration from non-generic to generic interfaces
public class MigrationExample
{
public void ConfigureServices(IServiceCollection services)
{
// Option 1: Use generic interface directly
services.AddSingleton<IParameterValidator<MyParams>, DefaultParameterValidator<MyParams>>();
services.AddSingleton<IResourceCache<MyResource>, InMemoryResourceCache<MyResource>>();
// Option 2: Keep existing non-generic registrations (fully backward compatible)
services.AddSingleton<IParameterValidator, DefaultParameterValidator>();
services.AddSingleton<IResourceCache, InMemoryResourceCache>();
// Option 3: Convert existing non-generic to generic using extension methods
var nonGenericValidator = serviceProvider.GetService<IParameterValidator>();
var typedValidator = nonGenericValidator.ForType<MyParams>(); // Extension method conversion
}
}
Override the ErrorMessages property in your tools to provide context-specific error messages and recovery guidance:
public class DatabaseTool : McpToolBase<DbParams, DbResult>
{
// Custom error message provider
protected override ErrorMessageProvider ErrorMessages => new DatabaseErrorMessageProvider();
// ... tool implementation
}
public class DatabaseErrorMessageProvider : ErrorMessageProvider
{
public override string ToolExecutionFailed(string toolName, string details)
{
return $"Database operation '{toolName}' failed: {details}. Check connection status.";
}
public override RecoveryInfo GetRecoveryInfo(string errorCode, string? context = null, Exception? exception = null)
{
return errorCode switch
{
"CONNECTION_FAILED" => new RecoveryInfo
{
Steps = new[]
{
"Verify database connection string",
"Check network connectivity",
"Ensure database server is running"
},
SuggestedActions = new[]
{
new SuggestedAction
{
Tool = "test_connection",
Description = "Test database connectivity",
Parameters = new { timeout = 30 }
}
}
},
_ => base.GetRecoveryInfo(errorCode, context, exception)
};
}
}
Configure token limits per tool, category, or globally using the server builder:
var builder = new McpServerBuilder()
.WithServerInfo("My Server", "1.0.0")
.ConfigureTokenBudgets(budgets =>
{
// Tool-specific limits (highest priority)
budgets.ForTool<LargeDataTool>()
.MaxTokens(20000)
.WarningThreshold(16000)
.WithStrategy(TokenLimitStrategy.Truncate)
.Apply();
budgets.ForTool<SearchTool>()
.MaxTokens(5000)
.WithStrategy(TokenLimitStrategy.Throw)
.Apply();
// Category-based limits (medium priority)
budgets.ForCategory(ToolCategory.Analysis)
.MaxTokens(15000)
.WarningThreshold(12000)
.Apply();
budgets.ForCategory(ToolCategory.Query)
.MaxTokens(8000)
.Apply();
// Default limits (lowest priority)
budgets.Default()
.MaxTokens(10000)
.WarningThreshold(8000)
.WithStrategy(TokenLimitStrategy.Warn)
.EstimationMultiplier(1.2) // Conservative estimates
.Apply();
});
public class HighVolumeAnalysisTool : McpToolBase<AnalysisParams, AnalysisResult>
{
// Override the default token budget for this specific tool
protected override TokenBudgetConfiguration TokenBudget => new()
{
MaxTokens = 50000,
WarningThreshold = 40000,
Strategy = TokenLimitStrategy.Truncate,
EstimationMultiplier = 1.5
};
// ... tool implementation
}
The framework provides several validation helpers in the McpToolBase class to simplify parameter validation:
public class DataProcessingTool : McpToolBase<DataParams, DataResult>
{
protected override async Task<DataResult> ExecuteInternalAsync(
DataParams parameters,
CancellationToken cancellationToken)
{
// Validate required parameters (throws ValidationException if null/empty)
var filePath = ValidateRequired(parameters.FilePath, nameof(parameters.FilePath));
var query = ValidateRequired(parameters.Query, nameof(parameters.Query));
// Validate positive numbers
var maxResults = ValidatePositive(parameters.MaxResults, nameof(parameters.MaxResults));
// Validate ranges
var priority = ValidateRange(parameters.Priority, 1, 10, nameof(parameters.Priority));
// Validate collections aren't empty
var tags = ValidateNotEmpty(parameters.Tags, nameof(parameters.Tags));
// All validation passed - process the data
return await ProcessDataAsync(filePath, query, maxResults, priority, tags);
}
}
| Helper | Purpose | Throws |
|---|---|---|
ValidateRequired<T>(value, paramName) | Ensures value is not null or empty string | ValidationException |
ValidatePositive(value, paramName) | Ensures numeric value > 0 | ValidationException |
ValidateRange(value, min, max, paramName) | Ensures value is within range | ValidationException |
ValidateNotEmpty<T>(collection, paramName) | Ensures collection has items | ValidationException |
The framework provides helpers to create standardized error results with recovery information:
public class DatabaseTool : McpToolBase<DbParams, DbResult>
{
protected override async Task<DbResult> ExecuteInternalAsync(
DbParams parameters,
CancellationToken cancellationToken)
{
try
{
var connectionString = ValidateRequired(parameters.ConnectionString, nameof(parameters.ConnectionString));
// Attempt database operation
var result = await ExecuteDatabaseQuery(connectionString, parameters.Query);
return new DbResult
{
Success = true,
Operation = "database_query",
Data = result
};
}
catch (SqlException ex) when (ex.Number == 2) // Connection timeout
{
// Create standardized error with recovery steps
return new DbResult
{
Success = false,
Operation = "database_query",
Error = CreateErrorResult(
"database_query",
$"Database connection timeout: {ex.Message}",
"Verify database server is running and accessible"
)
};
}
catch (ArgumentException ex)
{
// Create validation error with specific guidance
return new DbResult
{
Success = false,
Operation = "database_query",
Error = CreateValidationErrorResult(
"database_query",
"connectionString",
"Must be a valid SQL Server connection string"
)
};
}
}
}
| Helper | Purpose | Returns |
|---|---|---|
CreateErrorResult(operation, error, recoveryStep?) | Creates ErrorInfo with recovery guidance | ErrorInfo |
CreateValidationErrorResult(operation, paramName, requirement) | Creates validation-specific error | ErrorInfo |
CreateSuccessResult<T>(data, message?) | Creates successful ToolResult<T> | ToolResult<T> |
CreateErrorResult<T>(errorMessage, errorCode?) | Creates failed ToolResult<T> | ToolResult<T> |
public class FileAnalysisTool : McpToolBase<FileAnalysisParams, FileAnalysisResult>
{
protected override async Task<FileAnalysisResult> ExecuteInternalAsync(
FileAnalysisParams parameters,
CancellationToken cancellationToken)
{
try
{
var filePath = ValidateRequired(parameters.FilePath, nameof(parameters.FilePath));
if (!File.Exists(filePath))
{
// Return AI-friendly error with recovery steps
return new FileAnalysisResult
{
Success = false,
Operation = "analyze_file",
Error = new ErrorInfo
{
Code = "FILE_NOT_FOUND",
Message = $"File not found: {filePath}",
Recovery = new RecoveryInfo
{
Steps = new[]
{
"Verify the file path is correct",
"Check if the file exists",
"Ensure you have read permissions"
},
SuggestedActions = new[]
{
new SuggestedAction
{
Tool = "list_files",
Description = "List files in directory",
Parameters = new { path = Path.GetDirectoryName(filePath) }
}
}
}
}
};
}
// Perform analysis...
var analysis = await AnalyzeFileAsync(filePath);
return new FileAnalysisResult
{
Success = true,
Operation = "analyze_file",
FilePath = filePath,
Analysis = analysis,
Insights = GenerateInsights(analysis),
Actions = GenerateNextActions(analysis)
};
}
catch (UnauthorizedAccessException ex)
{
return CreateErrorResult(
"PERMISSION_DENIED",
$"Access denied: {ex.Message}",
new[] { "Check file permissions", "Run with appropriate privileges" }
);
}
}
}
The framework now provides automatic singleton-level caching for resources, solving the lifetime mismatch between scoped providers and singleton registry:
// Resource caching is automatically configured by McpServerBuilder
// No additional setup required - just implement your provider!
public class SearchResultResourceProvider : IResourceProvider
{
private readonly ISearchService _searchService; // Can be scoped!
public SearchResultResourceProvider(ISearchService searchService)
{
_searchService = searchService; // Scoped dependency is OK
}
public string Scheme => "search-results";
public string Name => "Search Results Provider";
public string Description => "Provides search result resources";
public bool CanHandle(string uri) =>
uri.StartsWith($"{Scheme}://");
public async Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken ct)
{
// No need to implement caching - framework handles it!
var sessionId = ExtractSessionId(uri);
var results = await _searchService.LoadResultsAsync(sessionId);
return new ReadResourceResult
{
Contents = new List<ResourceContent>
{
new ResourceContent
{
Uri = uri,
Text = JsonSerializer.Serialize(results),
MimeType = "application/json"
}
}
};
}
public async Task<List<Resource>> ListResourcesAsync(CancellationToken ct)
{
// List available resources
var sessions = await _searchService.GetActiveSessionsAsync();
return sessions.Select(s => new Resource
{
Uri = $"{Scheme}://{s.Id}",
Name = $"Search Results {s.Id}",
Description = $"Results for query: {s.Query}",
MimeType = "application/json"
}).ToList();
}
}
// Register your provider - caching is automatic!
builder.Services.AddScoped<IResourceProvider, SearchResultResourceProvider>();
// Optional: Configure cache settings
builder.Services.Configure<ResourceCacheOptions>(options =>
{
options.DefaultExpiration = TimeSpan.FromMinutes(10);
options.SlidingExpiration = TimeSpan.FromMinutes(5);
options.MaxSizeBytes = 200 * 1024 * 1024; // 200 MB
});
// Add the TokenOptimization package
// <PackageReference Include="COA.Mcp.Framework.TokenOptimization" Version="1.7.17" />
public class SearchTool : McpToolBase<SearchParams, SearchResult>
{
protected override async Task<SearchResult> ExecuteInternalAsync(
SearchParams parameters,
CancellationToken cancellationToken)
{
var results = await SearchAsync(parameters.Query);
// Use token management from base class
return await ExecuteWithTokenManagement(async () =>
{
// Automatically handles token limits
return new SearchResult
{
Success = true,
Results = results, // Auto-truncated if needed
TotalCount = results.Count,
ResourceUri = results.Count > 100 ?
await StoreAsResourceAsync(results) : null
};
});
}
}
using COA.Mcp.Framework.Testing;
using FluentAssertions;
[TestFixture]
public class WeatherToolTests
{
private WeatherTool _tool;
private Mock<IWeatherService> _weatherService;
[SetUp]
public void Setup()
{
_weatherService = new Mock<IWeatherService>();
_tool = new WeatherTool(_weatherService.Object);
}
[Test]
public async Task GetWeather_WithValidLocation_ReturnsWeatherData()
{
// Arrange
var parameters = new WeatherParameters
{
Location = "Seattle",
ForecastDays = 3
};
_weatherService
.Setup(x => x.GetWeatherAsync("Seattle", 3))
.ReturnsAsync(new WeatherData { /* ... */ });
// Act
var result = await _tool.ExecuteAsync(parameters);
// Assert
result.Should().NotBeNull();
result.Success.Should().BeTrue();
result.Location.Should().Be("Seattle");
result.Forecast.Should().HaveCount(3);
}
[Test]
public async Task GetWeather_WithMissingLocation_ReturnsError()
{
// Arrange
var parameters = new WeatherParameters { Location = null };
// Act
var result = await _tool.ExecuteAsync(parameters);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().NotBeNull();
result.Error.Code.Should().Be("VALIDATION_ERROR");
}
}
COA.Mcp.Framework/
├── Base/
│ └── McpToolBase.Generic.cs # Generic base class for tools
├── Server/
│ ├── McpServer.cs # Main server implementation
│ ├── McpServerBuilder.cs # Fluent builder API
│ └── Services/ # Auto-service management
│ ├── ServiceManager.cs # Service lifecycle management
│ ├── ServiceConfiguration.cs # Service config model
│ └── ServiceLifecycleHost.cs # IHostedService integration
├── Registration/
│ └── McpToolRegistry.cs # Unified tool registry
├── Interfaces/
│ ├── IMcpTool.cs # Tool interfaces
│ └── IResourceProvider.cs # Resource provider pattern
├── Models/
│ ├── ErrorModels.cs # Error handling models
│ └── ToolResultBase.cs # Base result class
└── Enums/
└── ToolCategory.cs # Tool categorization
The framework powers production MCP servers:
| Metric | Target | Actual |
|---|---|---|
| Build Time | <3s | 2.46s |
| Test Suite | 100% pass | 562/562 ✓ |
| Warnings | 0 | 0 ✓ |
| Framework Overhead | <5% | ~3% |
We welcome contributions! Key areas:
MIT License - see LICENSE file for details.
Built on experience from:
For comprehensive documentation, guides, and examples, see the Documentation Hub.
Ready to build your MCP server? Clone the repo and check out the examples:
git clone https://github.com/anortham/COA-Mcp-Framework.git
cd COA-Mcp-Framework/examples/SimpleMcpServer
dotnet run
For detailed guidance, see CLAUDE.md for AI-assisted development tips.