COA.Mcp.Framework
2.1.6
See the version list below for details.
dotnet add package COA.Mcp.Framework --version 2.1.6
NuGet\Install-Package COA.Mcp.Framework -Version 2.1.6
<PackageReference Include="COA.Mcp.Framework" Version="2.1.6" />
<PackageVersion Include="COA.Mcp.Framework" Version="2.1.6" />
<PackageReference Include="COA.Mcp.Framework" />
paket add COA.Mcp.Framework --version 2.1.6
#r "nuget: COA.Mcp.Framework, 2.1.6"
#:package COA.Mcp.Framework@2.1.6
#addin nuget:?package=COA.Mcp.Framework&version=2.1.6
#tool nuget:?package=COA.Mcp.Framework&version=2.1.6
COA MCP Framework
A 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.
๐ Quick Start
Never used MCP before? Follow our 5-Minute Quickstart Guide ๐
Super Simple Example
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)
{
var builder = new McpServerBuilder()
.WithServerInfo("My First MCP Server", "1.0.0");
builder.RegisterToolType<EchoTool>();
await builder.RunAsync();
}
}
- Run it:
dotnet run
๐ That's it! You have a working MCP server.
Next Steps
Want more examples?
- Hello World Example - Even simpler
- Multiple Tools Example - Calculator, text processing, etc.
- Full-Featured Example - All the bells and whistles
Need help?
- 5-Minute Quickstart - Step-by-step tutorial
- Common Issues - Solutions to typical problems
- Transport Guide - STDIO vs HTTP vs WebSocket
Ready for production? See Advanced Features below.
๐ Professional Behavioral Guidance
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:
- ๐ฏ Professional Tool Promotion: Evidence-based comparisons show why server tools are better than built-ins
- ๐ Smart Workflow Enforcement: Three levels (Suggest/Recommend/StronglyUrge) for appropriate guidance strength
- ๐ซ Educational: Teaches optimal patterns with performance metrics, not emotional manipulation
- โก Token Efficient: Fewer back-and-forth corrections save tokens (43% reduction measured)
- ๐ง Context-Aware: Instructions adapt dynamically to server capabilities and available tools
- ๐ Performance Focused: Specific metrics like "100x faster" and "<500ms" provide concrete evidence
- ๐ ๏ธ Template-Driven: Load instructions from .scriban files with variable substitution
๐ 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 detection
๐ AI-Powered Middleware
Add 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:
- ๐ก๏ธ Type Safety: Catches undefined types before code generation fails
- ๐งช Quality Assurance: Enforces proper testing practices
- ๐ AI-Friendly: Provides clear error messages with recovery steps
- โก Performance: Intelligent caching with file modification detection
๐ฆ NuGet Packages
- COA.Mcp.Framework โ Core framework with MCP protocol included
- COA.Mcp.Protocol โ Low-level protocol types and JSON-RPC
- COA.Mcp.Client โ Strongly-typed C# client for MCP servers
- COA.Mcp.Framework.TokenOptimization โ Advanced token management and AI response optimization
- COA.Mcp.Framework.Testing โ Testing helpers, assertions, and benchmarks
- COA.Mcp.Framework.Templates โ Project templates for quick starts
- COA.Mcp.Framework.Migration โ Migration tools for updating from older versions
โจ Key Features
๐ Type-Safe Tool Development
- Generic base class
McpToolBase<TParams, TResult>
ensures compile-time type safety - Automatic parameter validation using data annotations
- No manual JSON parsing required
๐๏ธ Clean Architecture
- Single unified tool registry with automatic disposal
- Fluent server builder API
- Dependency injection support
- Clear separation of concerns
- IAsyncDisposable support for resource management
๐ก๏ธ Comprehensive Error Handling
- Standardized error models with
ErrorInfo
andRecoveryInfo
- AI-friendly error messages with recovery steps
- Built-in validation helpers -
ValidateRequired()
,ValidatePositive()
,ValidateRange()
,ValidateNotEmpty()
- Error result helpers -
CreateErrorResult()
,CreateValidationErrorResult()
with recovery steps - Customizable error messages - Override
ErrorMessages
property for tool-specific guidance
๐ฏ Generic Type Safety
- Generic Parameter Validation:
IParameterValidator<TParams>
eliminates casting with strongly-typed validation - Generic Resource Caching:
IResourceCache<TResource>
supports any resource type with compile-time safety - Generic Response Building:
BaseResponseBuilder<TInput, TResult>
andAIOptimizedResponse<T>
prevent object casting - Backward Compatible: All generic interfaces include non-generic versions for seamless migration
๐ง Token Management
- Pre-estimation to prevent context overflow
- Progressive reduction for large datasets
- Smart truncation with resource URIs
- Per-tool token budgets - Configure limits via
ConfigureTokenBudgets()
in server builder - Hierarchical configuration - Tool-specific, category, and default budget settings
๐ Lifecycle Hooks & Middleware
- Extensible execution pipeline - Add cross-cutting concerns with simple middleware
- Built-in middleware - Logging, token counting, performance monitoring
- ๐ Type Verification Middleware - Prevents AI hallucinated types in code generation with intelligent caching
- ๐ TDD Enforcement Middleware - Enforces Test-Driven Development workflow (Red-Green-Refactor)
- Smart caching system - Session-scoped type verification with file modification invalidation
- Multi-platform test integration - Supports dotnet test, npm test, pytest, and more
- Custom middleware support - Implement
ISimpleMiddleware
for custom logic - Per-tool configuration - Override
ToolSpecificMiddleware
for tool-specific hooks - See Lifecycle Hooks Guide for detailed documentation
๐ฌ Interactive Prompts
- Guide users through complex operations with prompt templates
- Customizable arguments with validation
- Built-in message builders for system/user/assistant roles
- Variable substitution in templates
๐ Auto-Service Management (New)
- Automatic startup of background services
- Health monitoring and auto-restart capabilities
- Port conflict detection
- Support for multiple managed services
๐ Rapid Development
- Minimal boilerplate code
- Built-in validation helpers
- Comprehensive IntelliSense support
- Rich example projects
๐ฏ Client Library
Consuming MCP Servers with COA.Mcp.Client
The 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" });
Strongly-Typed Client Operations
// 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}ยฐ");
}
๐ Interactive Prompts
Creating Custom Prompts
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>();
Variable Substitution
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!"
๐ Lifecycle Hooks & Middleware
The framework provides a powerful middleware system for adding cross-cutting concerns to your tools:
Adding Middleware to 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...
}
Built-in Middleware
- TokenCountingSimpleMiddleware: Estimates and logs token usage
- LoggingSimpleMiddleware: Comprehensive execution logging
Custom Middleware
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;
}
}
Middleware Execution Flow
- Sort by Order (ascending): Lower numbers run first
- Before hooks (in order): middleware1 โ middleware2 โ middleware3
- Tool execution with validation and token management
- After hooks (reverse order): middleware3 โ middleware2 โ middleware1
- Error hooks (reverse order, if error occurs)
๐ For complete documentation and advanced examples, see Lifecycle Hooks Guide
๐ Getting Started
Working Example: SimpleMcpServer
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"
}
};
}
}
Setting Up Your Server
// 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();
Transport Configuration
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;
});
Configuration with Service Provider Access
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);
});
Migration from Manual BuildServiceProvider()
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:
- No duplicate singletons: Single DI container, proper singleton behavior
- No ASP0000 warnings: Eliminates compiler warnings about BuildServiceProvider
- Better performance: Reduced memory usage and object allocation
- Cleaner code: Follows .NET dependency injection best practices
Logging Configuration
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
});
Logging Categories
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
Quick Configuration Examples
// 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;
});
๐ Auto-Service Management
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();
Key Features:
- Automatic Startup: Services start automatically when the MCP server launches
- Health Monitoring: Periodic health checks with configurable intervals
- Auto-Restart: Automatic recovery from service failures with retry limits
- Port Detection: Checks if ports are already in use before starting
- Graceful Shutdown: Clean service termination when MCP server stops
- Multiple Services: Support for multiple auto-started services
Configuration Options:
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
}
Multiple Services Example:
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";
}
);
Custom Health Checks:
// 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:
- Dual-mode architectures: MCP servers that need to expose HTTP APIs
- Microservice coordination: Managing related services together
- Development environments: Simplified local development setup
- Federation scenarios: MCP servers that communicate with each other
๐ฅ Advanced Features
Generic Type Safety - Eliminate Object Casting
The framework provides generic versions of key interfaces to eliminate object casting and improve type safety:
Generic Parameter Validation
// 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);
}
}
Generic Resource Caching
// 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>>();
Generic Response Building
// 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
}
}
};
}
}
Migration Examples
// 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
}
}
Key Benefits
- ๐ฏ Compile-time Safety: Catch type errors at build time instead of runtime
- ๐ Better Performance: No boxing/unboxing or reflection-based casting
- ๐ง Enhanced IntelliSense: Full type information available in IDE
- ๐ Seamless Migration: Non-generic interfaces still work, upgrade at your own pace
- ๐ ๏ธ Cleaner Code: Eliminate try-catch blocks around casting operations
Customizable Error Messages
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)
};
}
}
Token Budget Configuration
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();
});
Token Budget Strategies
- Warn: Log warning and continue (default)
- Throw: Throw exception to prevent execution
- Truncate: Truncate output to stay within limits
- Ignore: No token limit enforcement
Per-Tool Token Budget Override
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
}
Built-in Validation Helpers
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);
}
}
Available Validation Helpers
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 |
Built-in Error Result Helpers
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"
)
};
}
}
}
Available Error Result Helpers
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> |
Error Handling with Recovery Steps
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" }
);
}
}
}
Resource Providers
Automatic Resource Caching (v1.4.8+)
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
});
Why Resource Caching?
- Solves lifetime mismatch: Scoped providers can work with singleton registry
- Improves performance: Automatic caching of expensive operations
- Memory efficient: Built-in size limits and expiration
- Transparent: No code changes needed in existing providers
- Resilient: Failures in cache don't affect core functionality
Token Optimization (with optional package)
// 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
};
});
}
}
Testing Your Tools
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");
}
}
๐ Documentation
Framework Structure
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
Key Components
- McpToolBase<TParams, TResult>: Generic base class for type-safe tool implementation
- McpServerBuilder: Fluent API for server configuration
- McpToolRegistry: Manages tool registration and discovery
- ToolResultBase: Standard result format with error handling
- IResourceProvider: Interface for custom resource providers
๐ Real-World Examples
The framework powers production MCP servers:
- CodeSearch MCP: File and text searching with Lucene indexing
- CodeNav MCP: C# code navigation using Roslyn
- SimpleMcpServer: Example project with calculator, data store, and system info tools
๐ Performance
Metric | Target | Actual |
---|---|---|
Build Time | โค๏ธs | 2.46s |
Test Suite | 100% pass | 562/562 โ |
Warnings | 0 | 0 โ |
Framework Overhead | <5% | ~3% |
๐ค Contributing
We welcome contributions! Key areas:
- Additional tool examples
- Performance optimizations
- Documentation improvements
- Test coverage expansion
๐ License
MIT License - see LICENSE file for details.
๐ Acknowledgments
Built on experience from:
- COA CodeSearch MCP - Token optimization patterns
- COA CodeNav MCP - Roslyn integration patterns
- The MCP community - Feedback and ideas
๐ Documentation
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.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- COA.Mcp.Protocol (>= 2.1.6)
- Microsoft.Extensions.Caching.Memory (>= 9.0.8)
- Microsoft.Extensions.DependencyInjection (>= 9.0.8)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.8)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.8)
- Microsoft.Extensions.Logging (>= 9.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.8)
- Microsoft.Extensions.Logging.Console (>= 9.0.8)
- Microsoft.Extensions.Options (>= 9.0.8)
- Scriban (>= 6.2.1)
- System.ComponentModel.Annotations (>= 5.0.0)
- System.Text.Json (>= 9.0.8)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on COA.Mcp.Framework:
Package | Downloads |
---|---|
COA.Mcp.Framework.TokenOptimization
Token optimization and AI-friendly response building for MCP servers. Includes token estimation, progressive reduction, and response caching. |
|
COA.Mcp.Framework.Testing
Comprehensive testing helpers for MCP servers including fluent assertions, test builders, and mock infrastructure. |
|
COA.Mcp.Framework.Migration
Migration tools for converting existing MCP projects to use COA.Mcp.Framework |
|
COA.Mcp.Client
Strongly-typed C# client library for interacting with MCP servers over HTTP/WebSocket |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
2.1.12 | 0 | 9/9/2025 |
2.1.10 | 46 | 9/9/2025 |
2.1.8 | 58 | 9/8/2025 |
2.1.6 | 72 | 9/7/2025 |
2.1.4 | 70 | 9/6/2025 |
2.1.1 | 68 | 9/6/2025 |
2.0.21 | 96 | 9/5/2025 |
2.0.18 | 145 | 9/4/2025 |
2.0.13 | 188 | 8/27/2025 |
2.0.8 | 278 | 8/25/2025 |
2.0.6 | 284 | 8/25/2025 |
1.7.22 | 166 | 8/24/2025 |
1.7.19 | 140 | 8/20/2025 |
1.7.2 | 152 | 8/12/2025 |
1.6.0 | 145 | 8/12/2025 |