Model Context Protocol (MCP) in ConnectSoft Microservice Template¶
Purpose & Overview¶
Model Context Protocol (MCP) is an open standard protocol that enables AI applications and large language models (LLMs) to securely access external tools, prompts, and resources. In the ConnectSoft Microservice Template, MCP provides a standardized way to expose microservice capabilities as tools and prompts that AI clients can discover and invoke, enabling AI-powered applications to interact with microservices in a structured, type-safe manner.
The Model Context Protocol integration provides:
- Tool Exposure: Expose microservice functions as callable tools for AI clients
- Prompt Templates: Provide reusable prompt templates that AI clients can render with dynamic data
- Standardized Protocol: Use the MCP specification for interoperability with AI clients
- Multiple Transports: Support both HTTP (SSE) and Stdio transports
- Type Safety: Strongly-typed tools and prompts with automatic discovery
- Self-Documenting: Tools and prompts are automatically documented through attributes
- AI Integration: Seamless integration with AI assistants, chatbots, and LLM applications
Model Context Protocol Philosophy
MCP enables microservices to expose their capabilities in a way that AI applications can understand and use. By providing tools and prompts through a standardized protocol, microservices become AI-ready, allowing AI assistants to interact with business logic, data, and operations in a structured, secure manner. This transforms microservices into "AI-native" services that can be discovered and used by AI applications.
Architecture Overview¶
MCP Integration Stack¶
AI Client / LLM Application
↓ (MCP Protocol)
MCP Server (ASP.NET Core)
├── HTTP Transport (SSE) - /mcp endpoint
└── Stdio Transport (Standard I/O)
↓
MCP Runtime
├── Tool Discovery (Reflection)
├── Prompt Discovery (Reflection)
└── Request Handling
↓
Tool/Prompt Implementations
├── DemoStringTools (Tools)
├── StringFormatPrompt (Prompts)
└── Business Logic Integration
Key Integration Points¶
| Layer | Component | Responsibility |
|---|---|---|
| ApplicationModel | ModelContextProtocolExtensions |
MCP server registration and configuration |
| ModelContextProtocol | DemoStringTools |
Tool implementations |
| ModelContextProtocol | StringFormatPrompt |
Prompt template implementations |
| Options | ModelContextProtocolServerOptions |
MCP server configuration |
| Options | McpHttpTransportOptions |
HTTP transport configuration |
MCP Concepts¶
Tools¶
Tools are callable functions that AI clients can invoke to perform operations. Tools accept parameters and return structured results.
Tool Characteristics: - Callable Functions: Methods that can be invoked by AI clients - Parameterized: Accept input parameters for operation - Structured Results: Return strongly-typed response objects - Self-Documenting: Described using attributes
Prompts¶
Prompts are template strings that can be rendered with dynamic data. Prompts help AI clients construct messages with context-specific information.
Prompt Characteristics: - Templates: Text templates with placeholders - Rendering: Dynamically filled with provided data - Message Format: Return chat messages for AI consumption - Reusable: Can be used across different contexts
Resources¶
Resources (optional) provide read-only access to data sources. Resources allow AI clients to read structured data.
Service Registration¶
MCP Server Setup¶
MCP server is registered via extension method:
Service Registration Details¶
The AddMicroserviceMCPServer() extension method configures the MCP server:
// ModelContextProtocolExtensions.cs
internal static IServiceCollection AddMicroserviceMCPServer(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// Create MCP server builder
IMcpServerBuilder mcpServerBuilder = services
.AddMcpServer(mcpOptions =>
{
mcpOptions.InitializationTimeout = OptionsExtensions.ModelContextProtocolServerOptions.InitializationTimeout;
mcpOptions.ProtocolVersion = OptionsExtensions.ModelContextProtocolServerOptions.ProtocolVersion;
mcpOptions.ScopeRequests = OptionsExtensions.ModelContextProtocolServerOptions.ScopeRequests;
});
// Configure transport
if (OptionsExtensions.ModelContextProtocolServerOptions.McpServerTransportType == McpServerTransportType.Stdio)
{
mcpServerBuilder.WithStdioServerTransport();
}
else if (OptionsExtensions.ModelContextProtocolServerOptions.McpServerTransportType == McpServerTransportType.Http)
{
mcpServerBuilder.WithHttpTransport(configure =>
{
configure.Stateless = OptionsExtensions.ModelContextProtocolServerOptions.McpHttpTransport.Stateless;
configure.PerSessionExecutionContext = OptionsExtensions.ModelContextProtocolServerOptions.McpHttpTransport.PerSessionExecutionContext;
configure.IdleTimeout = OptionsExtensions.ModelContextProtocolServerOptions.McpHttpTransport.IdleTimeout;
configure.MaxIdleSessionCount = OptionsExtensions.ModelContextProtocolServerOptions.McpHttpTransport.MaxIdleSessionCount;
});
}
// Discover tools and prompts from assemblies
mcpServerBuilder.WithToolsFromAssembly(typeof(DemoStringTools).Assembly);
mcpServerBuilder.WithPromptsFromAssembly(typeof(StringFormatPrompt).Assembly);
return services;
}
Endpoint Mapping¶
MCP server endpoint is mapped in the routing configuration:
// MicroserviceRegistrationExtensions.cs
private static void MapEndpoints(this IEndpointRouteBuilder endpoints)
{
// ... other endpoints ...
#if UseMCP
endpoints.MapMicroserviceMCPServer();
#endif
}
// ModelContextProtocolExtensions.cs
internal static IEndpointRouteBuilder MapMicroserviceMCPServer(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapMcp("/mcp");
return endpoints;
}
Endpoint: /mcp (default)
Configuration¶
MCP Server Configuration¶
MCP server configuration in appsettings.json:
{
"ModelContextProtocolServer": {
"McpServerTransportType": "Http",
"InitializationTimeout": "00:01:00",
"ProtocolVersion": "2025-06-18",
"ScopeRequests": true,
"McpHttpTransport": {
"Stateless": false,
"PerSessionExecutionContext": false,
"IdleTimeout": "02:00:00",
"MaxIdleSessionCount": 10000
}
}
}
Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
McpServerTransportType |
enum |
Required | Transport type: Stdio or Http |
InitializationTimeout |
TimeSpan |
00:01:00 |
Client-server initialization timeout |
ProtocolVersion |
string? |
null |
Protocol version (e.g., "2025-06-18") |
ScopeRequests |
bool |
true |
Create new service scope per request |
McpHttpTransport.Stateless |
bool |
false |
Run in stateless mode (no session state) |
McpHttpTransport.PerSessionExecutionContext |
bool |
false |
Use per-session execution context |
McpHttpTransport.IdleTimeout |
TimeSpan |
02:00:00 |
Session idle timeout |
McpHttpTransport.MaxIdleSessionCount |
int |
10000 |
Maximum idle sessions to track |
Transport Types¶
HTTP Transport¶
Characteristics: - Server-Sent Events (SSE) for streaming - HTTP-based, works over standard HTTP - Suitable for web applications and cloud deployments - Supports session management
Configuration:
{
"ModelContextProtocolServer": {
"McpServerTransportType": "Http",
"McpHttpTransport": {
"Stateless": false,
"IdleTimeout": "02:00:00",
"MaxIdleSessionCount": 10000
}
}
}
Stdio Transport¶
Characteristics: - Standard input/output streams - Suitable for local development and command-line tools - Used when MCP server is launched by client process - Direct process communication
Configuration:
Creating Tools¶
Tool Implementation¶
Tools are static methods marked with [McpServerTool] attribute:
// DemoStringTools.cs
using global::ModelContextProtocol.Server;
using System.ComponentModel;
[McpServerToolType]
[Description("Demonstration tool set that provides basic string utilities.")]
public static class DemoStringTools
{
[McpServerTool]
[Description("Echo a string (optionally uppercased) and return structured metadata.")]
public static EchoResponse Echo(
[Description("The text to be echoed back to the caller.")] string text,
[Description("When true, convert the text to upper case before returning.")] bool uppercase = false)
{
var processed = uppercase ? text?.ToUpperInvariant() : text;
return new EchoResponse
{
Text = processed ?? string.Empty,
OriginalLength = text?.Length ?? 0,
ExecutedAtUtc = DateTimeOffset.UtcNow,
};
}
}
Tool Attributes:
- [McpServerToolType]: Marks class as containing tools
- [McpServerTool]: Marks method as a tool
- [Description]: Provides tool and parameter descriptions
Tool Response Types¶
Tools return strongly-typed response objects:
// EchoResponse.cs
[DataContract]
[Description("Echo tool result payload with processed text and basic metadata.")]
public sealed class EchoResponse
{
[DataMember(Order = 1)]
[Description("Processed text that was echoed back.")]
public string Text { get; set; } = string.Empty;
[DataMember(Order = 2)]
[Description("Number of characters in the original input.")]
public int OriginalLength { get; set; }
[DataMember(Order = 3)]
[Description("Server-side UTC timestamp for when the tool executed.")]
public DateTimeOffset ExecutedAtUtc { get; set; }
}
Response Characteristics:
- Use [DataContract] for serialization
- Use [DataMember] with Order for field ordering
- Use [Description] for self-documentation
- Strongly-typed properties for type safety
Tool Discovery¶
Tools are automatically discovered from assemblies:
// ModelContextProtocolExtensions.cs
mcpServerBuilder.WithToolsFromAssembly(typeof(DemoStringTools).Assembly);
The MCP runtime scans the assembly for:
- Classes marked with [McpServerToolType]
- Static methods marked with [McpServerTool]
- Method parameters and return types for schema generation
Creating Prompts¶
Prompt Implementation¶
Prompts are instance methods marked with [McpServerPrompt] attribute:
// StringFormatPrompt.cs
using global::ModelContextProtocol.Server;
using Microsoft.Extensions.AI;
[McpServerPromptType]
[Description("Demonstration prompt provider that renders text templates using either named or numeric placeholders.")]
public sealed class StringFormatPrompt
{
private readonly ILogger<StringFormatPrompt> logger;
public StringFormatPrompt(ILogger<StringFormatPrompt> logger)
{
this.logger = logger;
}
[McpServerPrompt(Name = "render_named")]
[Description("Render a template with named placeholders like {Name}.")]
public IReadOnlyCollection<ChatMessage> RenderNamed(
[Description("Template with named placeholders, e.g., 'Hello {Name}'.")]
string template,
[Description("Dictionary of named values to fill the placeholders.")]
Dictionary<string, object?> args)
{
this.logger.LogInformation("MCP prompt 'render_named' invoked with template length {Length}.", template?.Length ?? 0);
var text = ReplaceNamedPlaceholders(template ?? string.Empty, args ?? new Dictionary<string, object?>());
return new ReadOnlyCollection<ChatMessage>(new List<ChatMessage>
{
new(ChatRole.User, text),
});
}
}
Prompt Attributes:
- [McpServerPromptType]: Marks class as containing prompts
- [McpServerPrompt(Name = "...")]: Marks method as a prompt with name
- [Description]: Provides prompt and parameter descriptions
Named Placeholders¶
Prompts can use named placeholders:
[McpServerPrompt(Name = "render_named")]
public IReadOnlyCollection<ChatMessage> RenderNamed(
string template,
Dictionary<string, object?> args)
{
// Template: "Hello {Name}! Your role is {Role}."
// Args: { "Name": "Dmitry", "Role": "Admin" }
// Result: "Hello Dmitry! Your role is Admin."
var text = ReplaceNamedPlaceholders(template, args);
return new ReadOnlyCollection<ChatMessage>(new List<ChatMessage>
{
new(ChatRole.User, text),
});
}
Numeric Placeholders¶
Prompts can use numeric placeholders (like string.Format):
[McpServerPrompt(Name = "render_numeric")]
[Description("Render a template with numeric placeholders like {0}, {1}.")]
public IReadOnlyCollection<ChatMessage> RenderNumeric(
[Description("Template with numeric placeholders, e.g., 'Hello {0}.'")]
string template,
[Description("Optional value for {0}.")] object? arg0 = null,
[Description("Optional value for {1}.")] object? arg1 = null,
[Description("Optional value for {2}.")] object? arg2 = null,
[Description("Optional value for {3}.")] object? arg3 = null)
{
// Template: "[{0}] says hi to [{1}]"
// Args: arg0 = "Dmitry", arg1 = "Cooper"
// Result: "[Dmitry] says hi to [Cooper]"
var args = TrimTrailingNulls(new object?[] { arg0, arg1, arg2, arg3 });
var text = string.Format(CultureInfo.InvariantCulture, template ?? string.Empty, args);
return new ReadOnlyCollection<ChatMessage>(new List<ChatMessage>
{
new(ChatRole.User, text),
});
}
Prompt Discovery¶
Prompts are automatically discovered from assemblies:
// ModelContextProtocolExtensions.cs
mcpServerBuilder.WithPromptsFromAssembly(typeof(StringFormatPrompt).Assembly);
The MCP runtime scans the assembly for:
- Classes marked with [McpServerPromptType]
- Instance methods marked with [McpServerPrompt]
- Method parameters for schema generation
Using Tools in Business Logic¶
Integrating Tools with Domain Services¶
Tools can call domain services and business logic:
[McpServerToolType]
[Description("Business operation tools for microservice operations.")]
public class BusinessTools
{
private readonly IMicroserviceAggregateRootsProcessor processor;
private readonly ILogger<BusinessTools> logger;
public BusinessTools(
IMicroserviceAggregateRootsProcessor processor,
ILogger<BusinessTools> logger)
{
this.processor = processor;
this.logger = logger;
}
[McpServerTool]
[Description("Create a new microservice aggregate root.")]
public async Task<CreateAggregateRootResponse> CreateAggregateRoot(
[Description("Object ID for the new aggregate root.")] Guid objectId,
[Description("Optional name for the aggregate root.")] string? name = null)
{
var input = new CreateMicroserviceAggregateRootInput
{
ObjectId = objectId
};
var result = await this.processor.CreateMicroserviceAggregateRoot(input);
return new CreateAggregateRootResponse
{
ObjectId = result.ObjectId,
CreatedAt = DateTimeOffset.UtcNow
};
}
}
Integration Points: - Tools can inject domain services via constructor - Tools can call processors, retrievers, and repositories - Tools can access configuration and other services - Tools execute within dependency injection scope
Important Notes:
- Instance methods (non-static) are required when using DI
- The MCP SDK resolves tool instances from the DI container when methods are not static
- ScopeRequests = true (default) ensures each tool invocation gets a fresh service scope
Tool Registration¶
Tools using dependency injection need to be registered in the DI container:
// ModelContextProtocolExtensions.cs
internal static IServiceCollection AddMicroserviceMCPServer(this IServiceCollection services)
{
// ... MCP server setup ...
// Register tool classes that use DI
// Use AddScoped for scoped lifetime (recommended for most cases)
services.AddScoped<BusinessTools>();
// Or use AddSingleton for singleton lifetime
// services.AddSingleton<BusinessTools>();
// Discover tools from assemblies
mcpServerBuilder.WithToolsFromAssembly(typeof(BusinessTools).Assembly);
return services;
}
Example: Wrapping a Domain Service as an MCP Tool¶
Here's a complete example showing how to wrap a domain service (e.g., IWeatherService) as an MCP tool:
1. Define the Domain Service Interface:
public interface IWeatherService
{
Task<WeatherForecast> GetForecastAsync(string location, int days);
Task<CurrentWeather> GetCurrentWeatherAsync(string location);
}
2. Create MCP Tool Class with DI:
[McpServerToolType]
[Description("Weather-related tools for retrieving weather information.")]
public class WeatherTools
{
private readonly IWeatherService weatherService;
private readonly ILogger<WeatherTools> logger;
public WeatherTools(
IWeatherService weatherService,
ILogger<WeatherTools> logger)
{
this.weatherService = weatherService;
this.logger = logger;
}
[McpServerTool]
[Description("Get weather forecast for a location.")]
public async Task<WeatherForecastResponse> GetForecast(
[Description("Location name (e.g., 'Seattle, WA')")] string location,
[Description("Number of days to forecast (1-7)")] int days = 5)
{
this.logger.LogInformation("Getting forecast for {Location}, {Days} days", location, days);
var forecast = await this.weatherService.GetForecastAsync(location, days);
return new WeatherForecastResponse
{
Location = location,
ForecastDays = days,
Forecasts = forecast.DailyForecasts.Select(f => new DayForecast
{
Date = f.Date,
High = f.High,
Low = f.Low,
Condition = f.Condition
}).ToList()
};
}
[McpServerTool]
[Description("Get current weather conditions for a location.")]
public async Task<CurrentWeatherResponse> GetCurrentWeather(
[Description("Location name (e.g., 'Seattle, WA')")] string location)
{
this.logger.LogInformation("Getting current weather for {Location}", location);
var current = await this.weatherService.GetCurrentWeatherAsync(location);
return new CurrentWeatherResponse
{
Location = location,
Temperature = current.Temperature,
Condition = current.Condition,
Humidity = current.Humidity,
WindSpeed = current.WindSpeed
};
}
}
3. Register Services in DI Container:
// In Program.cs or Startup.cs
services.AddScoped<IWeatherService, WeatherService>();
services.AddScoped<WeatherTools>(); // Register the tool class
#if UseMCP
services.AddMicroserviceMCPServer();
#endif
4. Response Types:
public class WeatherForecastResponse
{
public string Location { get; set; } = string.Empty;
public int ForecastDays { get; set; }
public List<DayForecast> Forecasts { get; set; } = new();
}
public class DayForecast
{
public DateTime Date { get; set; }
public int High { get; set; }
public int Low { get; set; }
public string Condition { get; set; } = string.Empty;
}
public class CurrentWeatherResponse
{
public string Location { get; set; } = string.Empty;
public int Temperature { get; set; }
public string Condition { get; set; } = string.Empty;
public int Humidity { get; set; }
public int WindSpeed { get; set; }
}
Key Points:
- ✅ Tool class uses constructor injection for IWeatherService
- ✅ Tool methods are instance methods (not static) to enable DI
- ✅ Tool class is registered in DI container (AddScoped<WeatherTools>())
- ✅ Domain service is registered in DI container (AddScoped<IWeatherService, WeatherService>())
- ✅ ScopeRequests = true ensures proper scoped service resolution
- ✅ Tools can access other services (e.g., ILogger) via DI
Observability¶
Observability is a critical aspect of MCP tool implementation, providing visibility into tool invocations through structured logs, distributed traces, and metrics. The template provides comprehensive observability infrastructure for MCP tools.
Overview¶
The observability infrastructure for MCP tools includes:
- Structured Logging: Captures detailed event information with context
- Distributed Tracing: OpenTelemetry spans for request flow tracking
- Metrics: Quantitative measurements (counters, histograms) for performance monitoring
All three pillars work together to provide complete visibility into tool execution, performance, and failures.
Structured Logging¶
Structured logging provides detailed, queryable logs for tool invocations using ILogger<T>.
Injection:
public class WeatherTools
{
private readonly ILogger<WeatherTools> logger;
public WeatherTools(ILogger<WeatherTools> logger)
{
this.logger = logger;
}
}
Usage Pattern:
[McpServerTool]
public EchoResponse Echo(string text, bool uppercase = false)
{
this.logger.LogInformation(
"MCP tool 'Echo' invoked with text length {TextLength}, uppercase: {Uppercase}",
text?.Length ?? 0,
uppercase);
try
{
// Tool logic...
this.logger.LogInformation(
"MCP tool 'Echo' completed successfully in {Duration}ms",
stopwatch.ElapsedMilliseconds);
return result;
}
catch (Exception ex)
{
this.logger.LogError(ex, "MCP tool 'Echo' failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
throw;
}
}
Best Practices:
- ✅ Use structured properties (e.g., {TextLength}, {Duration}) instead of string interpolation
- ✅ Log at appropriate levels: Information for normal flow, Warning for validation failures, Error for exceptions
- ✅ Include context: tool name, parameters, duration, result
- ✅ Use exception logging: logger.LogError(ex, "message") includes exception details
Distributed Tracing¶
Distributed tracing provides request flow visibility across services using OpenTelemetry ActivitySource.
ActivitySource:
The template provides McpToolActivitySource for creating spans:
using ConnectSoft.MicroserviceTemplate.ModelContextProtocol;
[McpServerTool]
public EchoResponse Echo(string text, bool uppercase = false)
{
using var activity = McpToolActivitySource.StartToolInvocationActivity("Echo", "DemoStringTools");
try
{
// Tool logic...
activity?.SetStatus(ActivityStatusCode.Ok);
activity?.SetTag("mcp.tool.result.length", result.Text.Length);
return result;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.AddException(ex);
throw;
}
}
Activity Tags:
- mcp.tool.name: Tool name (automatically set)
- mcp.tool.type: Tool class/type (automatically set)
- mcp.tool.result.*: Custom result tags
- mcp.tool.location: Location parameter (example)
- mcp.tool.forecast_days: Forecast days parameter (example)
Best Practices:
- ✅ Always wrap tool execution in using var activity
- ✅ Set status: ActivityStatusCode.Ok for success, ActivityStatusCode.Error for failures
- ✅ Record exceptions: activity?.AddException(ex) includes full exception details
- ✅ Add custom tags for filtering and analysis
- ✅ Keep tag values low-cardinality (avoid unique IDs in tags)
Metrics¶
Metrics provide quantitative measurements of tool performance using McpToolMetrics.
Metrics Class:
The McpToolMetrics class provides:
- mcp_tool_invocations_total: Counter for total invocations
- mcp_tool_invocations_failed_total: Counter for failed invocations
- mcp_tool_invocation_duration_seconds: Histogram for execution duration
Injection:
public class WeatherTools
{
private readonly McpToolMetrics metrics;
public WeatherTools(McpToolMetrics metrics)
{
this.metrics = metrics;
}
}
Manual Recording:
[McpServerTool]
public EchoResponse Echo(string text, bool uppercase = false)
{
var stopwatch = Stopwatch.StartNew();
try
{
// Tool logic...
this.metrics.RecordToolInvocationSuccess("Echo", "DemoStringTools", stopwatch.Elapsed);
return result;
}
catch (Exception ex)
{
this.metrics.RecordToolInvocationFailure("Echo", "DemoStringTools", stopwatch.Elapsed, "exception");
throw;
}
}
DurationScope Helper: For automatic duration tracking:
[McpServerTool]
public async Task<CurrentWeatherResponse> GetCurrentWeather(string location)
{
using var durationScope = this.metrics.TimeToolInvocation("GetCurrentWeather", "ObservableWeatherTools");
// Tool logic automatically timed on dispose
return await GetWeatherAsync(location);
}
Metrics Tags:
- tool_name: Name of the tool (required)
- tool_type: Type/class of the tool (optional)
- result: "success" or "failure"
- reason: Low-cardinality failure reason (validation, timeout, exception)
Best Practices: - ✅ Record success/failure for every invocation - ✅ Include duration for performance analysis - ✅ Use low-cardinality reason tags (validation, timeout, exception) - ✅ Use DurationScope for automatic timing - ✅ Record metrics even when exceptions occur
Complete Example¶
Here's a complete example showing all three observability pillars together:
[McpServerToolType]
[Description("Weather-related tools demonstrating comprehensive observability.")]
public class ObservableWeatherTools
{
private readonly ILogger<ObservableWeatherTools> logger;
private readonly McpToolMetrics metrics;
public ObservableWeatherTools(
ILogger<ObservableWeatherTools> logger,
McpToolMetrics metrics)
{
this.logger = logger;
this.metrics = metrics;
}
[McpServerTool]
[Description("Get weather forecast for a location.")]
public async Task<WeatherForecastResponse> GetForecast(string location, int days = 5)
{
using var activity = McpToolActivitySource.StartToolInvocationActivity("GetForecast", "ObservableWeatherTools");
var stopwatch = Stopwatch.StartNew();
try
{
// Structured logging
this.logger.LogInformation(
"MCP tool 'GetForecast' invoked for location {Location}, days: {Days}",
location,
days);
// Tracing tags
activity?.SetTag("mcp.tool.location", location);
activity?.SetTag("mcp.tool.forecast_days", days);
// Validation
if (string.IsNullOrWhiteSpace(location))
{
this.metrics.RecordToolInvocationFailure("GetForecast", "ObservableWeatherTools", stopwatch.Elapsed, "validation");
activity?.SetStatus(ActivityStatusCode.Error, "Location is required");
this.logger.LogWarning("MCP tool 'GetForecast' validation failed: location is empty");
throw new ArgumentException("Location is required", nameof(location));
}
// Tool logic
var result = await GetForecastAsync(location, days);
// Metrics - success
this.metrics.RecordToolInvocationSuccess("GetForecast", "ObservableWeatherTools", stopwatch.Elapsed);
// Tracing - success
activity?.SetStatus(ActivityStatusCode.Ok);
activity?.SetTag("mcp.tool.result.forecast_count", result.Forecasts.Count);
// Logging - success
this.logger.LogInformation(
"MCP tool 'GetForecast' completed successfully in {Duration}ms for location {Location}",
stopwatch.ElapsedMilliseconds,
location);
return result;
}
catch (ArgumentException)
{
// Validation errors already logged and recorded above
throw;
}
catch (Exception ex)
{
// Metrics - failure
this.metrics.RecordToolInvocationFailure("GetForecast", "ObservableWeatherTools", stopwatch.Elapsed, "exception");
// Tracing - failure
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.AddException(ex);
// Logging - failure
this.logger.LogError(ex,
"MCP tool 'GetForecast' failed after {Duration}ms for location {Location}",
stopwatch.ElapsedMilliseconds,
location);
throw;
}
}
}
Key Patterns: 1. ✅ Start activity at method entry 2. ✅ Start stopwatch for duration tracking 3. ✅ Log invocation start with structured properties 4. ✅ Set activity tags for filtering 5. ✅ Record metrics on success/failure 6. ✅ Set activity status and record exceptions 7. ✅ Log completion with duration
Observability Best Practices¶
Logging: - Use structured properties, not string interpolation - Include context: tool name, parameters, duration - Log at appropriate levels - Always log exceptions with full context
Tracing: - Create spans for every tool invocation - Set status codes (Ok/Error) - Record exceptions for error analysis - Add custom tags for filtering (low-cardinality)
Metrics: - Record success/failure for every invocation - Include duration for performance analysis - Use low-cardinality tags (avoid unique IDs) - Use DurationScope for automatic timing
Integration: - All three pillars work together: logs provide details, traces show flow, metrics show trends - Trace IDs are automatically propagated to logs and metrics - Use consistent naming: tool name, tool type, result, reason
Client Usage¶
MCP Client Connection¶
AI clients connect to MCP server using MCP client SDK:
// Client-side code
using ModelContextProtocol.Client;
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://api.example.com/mcp");
var clientTransport = new SseClientTransport(
transportOptions: new SseClientTransportOptions
{
Endpoint = httpClient.BaseAddress,
ConnectionTimeout = TimeSpan.FromMinutes(1),
Name = "My MCP Client",
TransportMode = HttpTransportMode.AutoDetect,
},
httpClient: httpClient,
loggerFactory: loggerFactory);
var mcpClient = await McpClientFactory.CreateAsync(
clientTransport: clientTransport,
clientOptions: new McpClientOptions
{
InitializationTimeout = TimeSpan.FromMinutes(1),
},
loggerFactory: loggerFactory);
Calling Tools¶
// List available tools
var tools = await McpClientExtensions.ListToolsAsync(mcpClient);
// Call a tool
var result = await mcpClient.CallToolAsync(
name: "Echo",
arguments: new Dictionary<string, object?>
{
["text"] = "Hello, World!",
["uppercase"] = true
});
// Access result
var echoResponse = result.Content?.ToObjectFromJson<EchoResponse>();
Console.WriteLine($"Echoed: {echoResponse?.Text}");
Rendering Prompts¶
// List available prompts
var prompts = await McpClientExtensions.ListPromptsAsync(mcpClient);
// Render a prompt
var promptResult = await mcpClient.GetPromptAsync(
name: "render_named",
arguments: new Dictionary<string, object?>
{
["template"] = "Hello {Name}! Your role is {Role}.",
["args"] = new Dictionary<string, object?>
{
["Name"] = "Dmitry",
["Role"] = "Admin"
}
});
// Access rendered messages
var messages = promptResult.Messages;
foreach (var message in messages)
{
Console.WriteLine($"{message.Role}: {message.Content}");
}
Testing¶
Unit Testing Tools¶
Test tools independently:
[TestMethod]
public void Echo_WithUppercase_ShouldReturnUppercase()
{
// Arrange
var text = "hello";
var uppercase = true;
// Act
var result = DemoStringTools.Echo(text, uppercase);
// Assert
Assert.AreEqual("HELLO", result.Text);
Assert.AreEqual(5, result.OriginalLength);
}
Integration Testing with MCP Client¶
Test MCP server with real MCP client:
[TestMethod]
public async Task MCP_Server_Should_Expose_Echo_Tool()
{
// Arrange
var server = BeforeAfterTestRunHooks.ServerInstance;
var httpClient = server.CreateClient();
httpClient.BaseAddress = new Uri("https://localhost:7279/mcp");
var loggerFactory = server.Services.GetRequiredService<ILoggerFactory>();
var clientTransport = new SseClientTransport(
transportOptions: new SseClientTransportOptions
{
Endpoint = httpClient.BaseAddress,
ConnectionTimeout = TimeSpan.FromMinutes(1),
},
httpClient: httpClient,
loggerFactory: loggerFactory);
var mcpClient = await McpClientFactory.CreateAsync(
clientTransport: clientTransport,
clientOptions: new McpClientOptions(),
loggerFactory: loggerFactory);
// Act
var tools = await McpClientExtensions.ListToolsAsync(mcpClient);
var echoTool = tools.FirstOrDefault(t => t.Name == "Echo");
// Assert
Assert.IsNotNull(echoTool, "Echo tool should be exposed");
}
BDD Testing with Reqnroll¶
Test MCP features using Reqnroll:
// Feature file
Feature: MCP Echo Tool Feature
Verify that the MCP server exposes the Echo tool and returns structured results
Scenario: Call Echo tool with uppercase
Given the MCP server is available at "/mcp"
When I call MCP tool "Echo" with:
| text | uppercase |
| "hello" | true |
Then the tool result should contain:
| property | value |
| Text | HELLO |
| OriginalLength | 5 |
Best Practices¶
Do's¶
-
Use Descriptive Names and Descriptions
-
Use Strongly-Typed Responses
-
Handle Errors Gracefully
-
Use Dependency Injection for Complex Tools
-
Document Parameters Thoroughly
Don'ts¶
-
Don't Expose Internal Implementation Details
-
Don't Use Generic Response Types
-
Don't Ignore Errors
// ❌ BAD - Errors swallowed [McpServerTool] public static string Process() { try { /* ... */ } catch { return string.Empty; } } // ✅ GOOD - Errors returned in response [McpServerTool] public static ProcessResponse Process() { try { /* ... */ } catch (Exception ex) { return new ProcessResponse { Error = ex.Message }; } } -
Don't Use Complex Parameter Types
Input Validation and Guardrails¶
Input Validation¶
MCP SDK provides input validation through strongly-typed method signatures and JSON deserialization:
How It Works: - Tool parameters are defined as strongly-typed C# method parameters - JSON schemas are auto-generated from method signatures for tool discovery - Input validation occurs during JSON deserialization when the MCP client calls the tool - Type mismatches cause deserialization failures, which are surfaced as MCP-compliant errors
Example:
[McpServerTool]
public static EchoResponse Echo(
[Description("The text to be echoed back.")] string text,
[Description("Convert to uppercase.")] bool uppercase = false)
{
// 'text' is guaranteed to be a string (or null if nullable)
// 'uppercase' is guaranteed to be a boolean
// Invalid types in the request will fail during deserialization
return new EchoResponse { Text = text ?? string.Empty };
}
Limitations:
- No explicit pre-invocation schema validation layer
- No support for validation attributes (e.g., [Required], [Range], [StringLength])
- Validation happens at the deserialization boundary, not as a separate validation step
- For advanced validation, implement validation logic inside tool methods
Best Practice: For tools requiring complex validation, add explicit validation in the tool implementation:
[McpServerTool]
public static ProcessOrderResponse ProcessOrder(
[Description("Order ID (GUID format)")] string orderId,
[Description("Process immediately")] bool immediate)
{
// Explicit validation
if (string.IsNullOrWhiteSpace(orderId) || !Guid.TryParse(orderId, out _))
{
return new ProcessOrderResponse
{
Success = false,
Error = "Invalid order ID format. Expected GUID."
};
}
// Continue with processing...
}
Timeouts¶
MCP SDK provides session-level timeouts but does not support per-tool invocation timeouts:
Available Timeout Configurations:
- InitializationTimeout (Client-Server Handshake)
- Configurable in
ModelContextProtocolServerOptions - Default: 60 seconds
-
Controls how long the server waits for client responses during initialization
-
IdleTimeout (HTTP Session Idle Time)
- Configurable in
McpHttpTransportOptions - Default: 2 hours
- Controls how long an idle MCP session remains active
Limitations:
- No per-tool invocation timeout - Long-running tools could block indefinitely
- No timeout enforcement during tool execution
- No CancellationToken timeout pattern in tool execution
Best Practice: For tools that may run for extended periods, implement timeout logic within the tool:
[McpServerTool]
public static async Task<ProcessResponse> ProcessLongRunningTask(
[Description("Task ID")] string taskId,
CancellationToken cancellationToken)
{
// Use CancellationToken for timeout handling
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMinutes(5)); // 5-minute timeout
try
{
await ProcessTaskAsync(taskId, cts.Token);
return new ProcessResponse { Success = true };
}
catch (OperationCanceledException)
{
return new ProcessResponse
{
Success = false,
Error = "Operation timed out after 5 minutes"
};
}
}
Note: Per-invocation timeouts would require custom implementation or enhancement of the MCP SDK integration layer.
Error Handling¶
MCP SDK surfaces errors through MCP-compliant response structures. Best practice is to handle errors within tool implementations and return structured error information:
Error Response Pattern:
[McpServerTool]
public static ProcessResponse ProcessOrder(Guid orderId)
{
try
{
// Business logic
var result = ProcessOrderInternal(orderId);
return new ProcessResponse
{
Success = true,
OrderId = orderId,
Status = "Processed"
};
}
catch (ValidationException ex)
{
return new ProcessResponse
{
Success = false,
Error = ex.Message,
ErrorCode = "VALIDATION_ERROR"
};
}
catch (Exception ex)
{
// Log exception details
// Return user-friendly error message
return new ProcessResponse
{
Success = false,
Error = "An error occurred while processing the order.",
ErrorCode = "PROCESSING_ERROR"
};
}
}
Error Response Structure:
public class ProcessResponse
{
public bool Success { get; set; }
public string? Error { get; set; }
public string? ErrorCode { get; set; }
// Additional response fields...
}
Best Practices: - ✅ Always return structured response objects with error information - ✅ Catch exceptions and return error details in response - ✅ Use error codes for programmatic error handling - ✅ Provide user-friendly error messages - ✅ Log detailed exception information server-side - ❌ Don't let exceptions propagate unhandled (they may not be properly formatted) - ❌ Don't expose internal implementation details in error messages
MCP Protocol Compliance:
- Tool execution results are returned via CallToolResult.Content
- Errors should be included as part of the response content (not as separate error responses)
- The MCP SDK handles serialization of response objects automatically
Troubleshooting¶
Issue: Tools Not Discovered¶
Symptoms: Tools don't appear in client's tool list.
Solutions:
1. Verify [McpServerToolType] attribute on class
2. Verify [McpServerTool] attribute on method
3. Check assembly is included in WithToolsFromAssembly()
4. Ensure tools are in a referenced assembly
5. Verify tool class is registered in DI (if using instance methods)
Issue: Prompts Not Discovered¶
Symptoms: Prompts don't appear in client's prompt list.
Solutions:
1. Verify [McpServerPromptType] attribute on class
2. Verify [McpServerPrompt] attribute on method
3. Check assembly is included in WithPromptsFromAssembly()
4. Ensure prompt class is registered in DI
5. Verify prompt method returns IReadOnlyCollection<ChatMessage>
Issue: HTTP Transport Not Working¶
Symptoms: Client can't connect via HTTP.
Solutions:
1. Verify endpoint is mapped: endpoints.MapMcp("/mcp")
2. Check HTTP transport is configured in options
3. Verify McpHttpTransport options are set
4. Check firewall/network connectivity
5. Review logs for transport errors
Issue: Tool Execution Errors¶
Symptoms: Tools fail when called by clients.
Solutions: 1. Check tool method signature (parameters, return type) 2. Verify parameter types match client expectations 3. Ensure tool handles null/empty parameters 4. Check dependency injection is configured correctly 5. Review application logs for exceptions
Security Guidelines¶
Overview¶
Security is critical when exposing tools through the Model Context Protocol, as AI clients may invoke tools with unexpected inputs or in unintended ways. MCP tools are accessible to AI applications that may process user input from untrusted sources, making security considerations paramount.
Security First
Always assume that MCP tools can be called with malicious or malformed input. Design tools with defense-in-depth principles, validate all inputs, and never trust data from AI clients.
Tool Allowlists¶
Current State: The MCP SDK automatically discovers and exposes all tools marked with [McpServerTool] attributes. Selective tool exposure (allowlists) is not yet implemented at the template level.
Best Practices:
- Selective Tool Exposure
- Only mark tools intended for AI consumption with
[McpServerTool] - Use separate assemblies or namespaces for AI-exposed tools vs. internal tools
-
Review all tools before deployment to ensure none expose sensitive operations
-
Future Allowlist Implementation When tool allowlisting is implemented, configure which tools are exposed:
// Future: Configure allowed tools
mcpServerBuilder.WithToolsFromAssembly(typeof(DemoStringTools).Assembly)
.AllowTool("Echo")
.AllowTool("GetForecast");
// Other tools in the assembly will be hidden from AI clients
- Namespace Organization
// ✅ GOOD - Clear separation namespace MyMicroservice.ModelContextProtocol.PublicTools { [McpServerToolType] public class PublicTools { /* Safe for AI exposure */ } } namespace MyMicroservice.ModelContextProtocol.InternalTools { // No [McpServerToolType] - not exposed to AI public class InternalTools { /* Internal only */ } }
Input Validation and Sanitization¶
Beyond Type Safety: While MCP SDK provides type safety through deserialization, you must validate business logic and security constraints:
-
Validate All Inputs
// ✅ GOOD - Comprehensive validation [McpServerTool] public static ProcessFileResponse ProcessFile( [Description("File path to process")] string filePath) { // Validate and sanitize input if (string.IsNullOrWhiteSpace(filePath)) { return new ProcessFileResponse { Error = "File path is required" }; } // Prevent directory traversal attacks var sanitizedPath = Path.GetFileName(filePath); if (sanitizedPath != filePath) { return new ProcessFileResponse { Error = "Invalid file path" }; } // Additional validation: whitelist allowed extensions var extension = Path.GetExtension(sanitizedPath); if (extension != ".txt" && extension != ".json") { return new ProcessFileResponse { Error = "File type not allowed" }; } // Restrict to safe directory var safePath = Path.Combine("/safe/directory", sanitizedPath); // ... process file } -
Validate Range and Constraints
// ✅ GOOD - Range validation [McpServerTool] public static QueryResponse QueryData( [Description("Maximum number of results")] int maxResults) { // Prevent resource exhaustion if (maxResults < 1 || maxResults > 100) { return new QueryResponse { Error = "maxResults must be between 1 and 100" }; } // ... query data } -
Sanitize String Inputs
// ✅ GOOD - Sanitize user input [McpServerTool] public static SearchResponse Search( [Description("Search query")] string query) { // Remove potentially dangerous characters var sanitized = query?.Trim(); if (string.IsNullOrWhiteSpace(sanitized) || sanitized.Length > 1000) { return new SearchResponse { Error = "Invalid search query" }; } // Escape special characters for SQL/database queries // Use parameterized queries, never string concatenation // ... perform search }
Avoiding Dangerous Operations¶
Never expose these operations directly to AI clients:
-
File System Access
// ❌ BAD - Dangerous arbitrary file access [McpServerTool] public static FileContentResponse ReadFile(string filePath) { // NEVER allow arbitrary file paths return File.ReadAllText(filePath); // Vulnerable to path traversal } // ✅ GOOD - Restricted file access [McpServerTool] public static FileContentResponse ReadAllowedFile( [Description("Document ID from allowed list")] string documentId) { // Validate documentId is in allowed whitelist var allowedDocuments = GetAllowedDocuments(); if (!allowedDocuments.Contains(documentId)) { return new FileContentResponse { Error = "Document not found" }; } // Use pre-validated path var safePath = GetDocumentPath(documentId); return new FileContentResponse { Content = File.ReadAllText(safePath) }; } -
OS Command Execution
// ❌ BAD - Never expose command execution [McpServerTool] public static CommandResponse ExecuteCommand(string command) { // NEVER execute arbitrary commands var process = Process.Start(command); // Extremely dangerous! return new CommandResponse { Output = process.StandardOutput.ReadToEnd() }; } // ✅ GOOD - Specific, controlled operations [McpServerTool] public static HealthCheckResponse CheckSystemHealth() { // Only execute predefined, safe operations var healthStatus = SystemHealthChecker.Check(); return new HealthCheckResponse { Status = healthStatus }; } -
Network Access
// ❌ BAD - Arbitrary network access [McpServerTool] public static WebResponse FetchUrl(string url) { // NEVER allow arbitrary URLs var client = new HttpClient(); return client.GetAsync(url).Result; // SSRF vulnerability } // ✅ GOOD - Restricted network access [McpServerTool] public static ApiResponse CallAllowedApi( [Description("API endpoint name")] string endpointName) { // Whitelist allowed endpoints var allowedEndpoints = GetAllowedEndpoints(); if (!allowedEndpoints.TryGetValue(endpointName, out var url)) { return new ApiResponse { Error = "Endpoint not allowed" }; } // Only call whitelisted URLs var client = new HttpClient(); return client.GetAsync(url).Result; } -
Database Operations
// ❌ BAD - Arbitrary SQL execution [McpServerTool] public static QueryResponse ExecuteQuery(string sql) { // NEVER execute arbitrary SQL return database.ExecuteQuery(sql); // SQL injection risk } // ✅ GOOD - Parameterized queries with authorization [McpServerTool] public static QueryResponse QueryOrders( [Description("Customer ID")] string customerId) { // Validate authorization if (!IsAuthorizedToQueryCustomer(customerId)) { return new QueryResponse { Error = "Unauthorized" }; } // Use parameterized queries return database.Query<Order>( "SELECT * FROM Orders WHERE CustomerId = @customerId", new { customerId }); } -
Sensitive Data Exposure
// ❌ BAD - Exposing sensitive data [McpServerTool] public static UserResponse GetUser(string userId) { var user = userRepository.Get(userId); return new UserResponse { UserId = user.Id, Email = user.Email, PasswordHash = user.PasswordHash, // NEVER expose SSN = user.SSN, // NEVER expose CreditCard = user.CreditCard // NEVER expose }; } // ✅ GOOD - Filter sensitive data [McpServerTool] public static UserResponse GetUser(string userId) { var user = userRepository.Get(userId); return new UserResponse { UserId = user.Id, Email = user.Email, DisplayName = user.DisplayName // Sensitive fields intentionally omitted }; }
Sandboxing Considerations¶
For high-security environments, consider sandboxing MCP tool execution:
-
Resource Limits
// ✅ GOOD - Resource-constrained execution [McpServerTool] public async Task<ProcessResponse> ProcessData(string data) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // Timeout // Limit memory usage through streaming // Use memory-efficient algorithms // Monitor resource consumption return await ProcessWithLimits(data, cts.Token); } -
Isolated Execution
- Container Isolation: Run MCP server in isolated containers with restricted permissions
- Process Isolation: Execute tools in separate processes with limited capabilities
- Network Isolation: Use network policies to restrict outbound connections
-
File System Isolation: Use read-only filesystems or restricted directories
-
Runtime Sandboxing
// Example: Execute tool in isolated context public class SandboxedToolExecutor { public async Task<T> ExecuteInSandbox<T>( Func<Task<T>> toolExecution, TimeSpan timeout, long maxMemoryBytes) { // Implement sandboxing logic // - Monitor memory usage // - Enforce timeouts // - Catch and handle exceptions // - Log all operations } }
Rate Limiting and Quotas¶
Current State: The template includes MCP-specific rate limiting configuration that works alongside the global rate limiter. MCP rate limiting is automatically applied to the /mcp endpoint when both UseMCP and RateLimiting template parameters are enabled.
- MCP Rate Limiting Configuration
MCP rate limiting is configured in appsettings.json:
{
"RateLimiting": {
"EnableRateLimiting": true,
"GlobalLimiter": {
"Window": "00:01:00",
"PermitLimit": 5,
"AutoReplenishment": true,
"QueueLimit": 0
},
"McpLimiter": {
"Window": "00:01:00",
"PermitLimit": 100,
"AutoReplenishment": true,
"QueueLimit": 0
}
}
}
The McpLimiter configuration is independent from GlobalLimiter, allowing you to set different rate limits for MCP endpoints. When configured, a named rate limiting policy "MCP" is automatically created and applied to the /mcp endpoint.
-
How MCP Rate Limiting Works
-
Partitioning Strategy: MCP rate limiting uses the same partitioning strategy as the global limiter (IP address or
X-Test-Idheader) - Independent Limits: MCP endpoints have their own rate limit separate from the global limit
- Policy Name: The policy is named "MCP" and is automatically applied to all MCP endpoints
-
Automatic Application: Rate limiting is automatically applied when both
EnableRateLimitingistrueandMcpLimiteris configured (required when MCP is enabled) -
Implementation Details
The MCP rate limiting policy is configured in RateLimitingExtensions.cs:
#if UseMCP
// Configure MCP-specific rate limiting policy
options.AddFixedWindowLimiter("MCP", limiterOptions =>
{
limiterOptions.PermitLimit = OptionsExtensions.RateLimitingOptions.McpLimiter.PermitLimit;
limiterOptions.Window = OptionsExtensions.RateLimitingOptions.McpLimiter.Window;
limiterOptions.AutoReplenishment = OptionsExtensions.RateLimitingOptions.McpLimiter.AutoReplenishment;
limiterOptions.QueueLimit = OptionsExtensions.RateLimitingOptions.McpLimiter.QueueLimit;
});
#endif
And automatically applied to the MCP endpoint in ModelContextProtocolExtensions.cs:
var routeHandlerBuilder = endpoints.MapMcp("/mcp");
#if RateLimiting
// Apply MCP-specific rate limiting policy if rate limiting is enabled
if (OptionsExtensions.RateLimitingOptions.EnableRateLimiting)
{
routeHandlerBuilder.RequireRateLimiting("MCP");
}
#endif
-
Per-Tool Rate Limiting
// ✅ GOOD - Rate limit specific tools [McpServerTool] public static ExpensiveOperationResponse ExpensiveOperation(string input) { // Check rate limit before expensive operation if (!rateLimiter.TryAcquire("expensive_operation")) { return new ExpensiveOperationResponse { Error = "Rate limit exceeded. Please try again later." }; } // ... perform operation } -
Quota Management
- Implement per-user or per-session quotas
- Track resource usage (CPU, memory, API calls)
- Enforce daily/weekly limits
- Provide quota status in responses
Authentication and Authorization¶
Current State: MCP endpoints are not automatically protected by authentication. You must implement authentication/authorization for production use.
-
Secure MCP Endpoints
-
API Key Authentication
// Implement API key validation middleware public class ApiKeyAuthenticationMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (!context.Request.Headers.TryGetValue("X-API-Key", out var apiKey)) { context.Response.StatusCode = 401; return; } if (!IsValidApiKey(apiKey)) { context.Response.StatusCode = 401; return; } await next(context); } } -
Role-Based Tool Access
-
Audit Logging
// ✅ GOOD - Log all tool invocations [McpServerTool] public static ProcessResponse Process(string input) { var userId = GetCurrentUserId(); logger.LogInformation( "Tool 'Process' invoked by user {UserId} with input length {Length}", userId, input?.Length ?? 0); // ... process // Log result auditLogger.LogToolInvocation( toolName: "Process", userId: userId, input: input, result: "Success", timestamp: DateTime.UtcNow); }
Safe Practices¶
- Principle of Least Privilege
- Grant tools only the minimum permissions needed
- Use service accounts with restricted permissions
-
Avoid running tools with elevated privileges
-
Input Sanitization
// ✅ GOOD - Sanitize all user input private static string SanitizeInput(string input) { if (string.IsNullOrWhiteSpace(input)) return string.Empty; // Remove control characters var sanitized = new string(input .Where(c => !char.IsControl(c)) .ToArray()); // Limit length if (sanitized.Length > 1000) sanitized = sanitized.Substring(0, 1000); return sanitized.Trim(); } -
Output Filtering
-
Error Message Security
// ❌ BAD - Leaking sensitive information catch (Exception ex) { return new ErrorResponse { Message = ex.ToString() }; // Includes stack traces, paths, etc. } // ✅ GOOD - Safe error messages catch (Exception ex) { logger.LogError(ex, "Tool execution failed"); return new ErrorResponse { Message = "An error occurred. Please contact support.", ErrorCode = "INTERNAL_ERROR" }; } -
Logging Best Practices
// ✅ GOOD - Log without sensitive data logger.LogInformation( "Tool 'ProcessOrder' invoked for order {OrderId} by user {UserId}", orderId, // OK to log userId); // OK to log // ❌ BAD - Logging sensitive data logger.LogInformation( "Tool 'ProcessOrder' invoked with data: {Data}", requestData); // May contain PII, passwords, etc. -
Timeouts and Cancellation
// ✅ GOOD - Always use timeouts [McpServerTool] public async Task<ProcessResponse> LongRunningOperation(string input) { using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); try { return await ProcessWithTimeout(input, cts.Token); } catch (OperationCanceledException) { return new ProcessResponse { Error = "Operation timed out" }; } }
Security Checklist¶
Before deploying MCP tools to production:
- All inputs are validated and sanitized
- No arbitrary file system access
- No arbitrary command execution
- No arbitrary network access (SSRF protection)
- Database queries use parameterization
- Sensitive data is filtered from responses
- Error messages don't leak sensitive information
- Rate limiting is configured for MCP endpoints
- Authentication/authorization is implemented
- All tool invocations are logged and audited
- Resource limits (timeouts, memory) are enforced
- Tools are tested with malicious inputs
- Security review is performed before deployment
Summary¶
Model Context Protocol (MCP) in the ConnectSoft Microservice Template provides:
- ✅ Tool Exposure: Expose microservice functions as AI-callable tools
- ✅ Prompt Templates: Provide reusable prompt templates for AI clients
- ✅ Standardized Protocol: Use MCP specification for interoperability
- ✅ Multiple Transports: Support HTTP (SSE) and Stdio transports
- ✅ Type Safety: Strongly-typed tools and prompts
- ✅ Self-Documenting: Automatic documentation through attributes
- ✅ DI Integration: Seamless dependency injection support
- ✅ Testing: Unit and integration testing support
By following these patterns, teams can:
- Expose AI-Ready Services: Make microservices discoverable and usable by AI applications
- Enable AI Integration: Allow AI assistants to interact with business logic
- Standardize Interactions: Use MCP protocol for consistent AI-to-service communication
- Maintain Type Safety: Leverage strongly-typed tools and prompts
- Test Thoroughly: Write comprehensive tests for MCP tools and prompts
The Model Context Protocol integration ensures that microservices can seamlessly integrate with AI applications, enabling AI assistants, chatbots, and LLM-powered tools to discover and use microservice capabilities in a structured, secure, and type-safe manner.