First, let’s address the obvious question.
Why am I calling this 14.2 instead of just Part 15?
Well, teaching how to build a production-grade MCP server definitely isn’t going to fit into a single article, is it?
So instead of cramming everything into one massive post, we’re turning this into a 4-part mini-series.
Yes, I know how that sounds.
A series… inside another series?
Umm. Yeah.
Anyway, here’s how this is going to work:
Schrödinger's AI is your invitation to look inside. Right now, AI feels like a mystery , wired like a brain, yet running on pure math.
Each article is a new layer of the box. We start with the first spark of an idea and move all the way to the models reshaping everything we thought we knew .
Explore the entire series Schrodingers-AI
I’d suggest cloning the code from my repository: review-my-code-mcp
It’ll make it easier to follow along with the project as we build it. That said, it’s not strictly required since we’ll be building everything step by step throughout the series.
![Schrödinger’s AI]()
Part 14.3: ReviewMyCode MCP Server: Rules & Extensibility
By now, you understand:
How MCP connects clients to your server
How ReviewAnalyzer runs rules and collects findings
How ReviewScorer calculates quality scores
But you haven't seen how rules are actually built. Let's do that in this article.
A "rule" in this system is a simple check: given source code, does it violate some pattern? If yes, return a finding. If no, return null.
We'll cover:
ICodeRule: The interface all rules implement
IRuleGroupProvider: How to organize rules by category
Concrete rule types: Token detection, regex, custom delegates
Building a provider: Grouping related rules
All eight categories: What each checks
Extending the system: How to add new rules without touching existing code
The Rule Interface: ICodeRule
Every rule implements one simple interface.
Create Rules/Abstractions/ICodeRule.cs:
using McpCodeReviewServer.Models;
namespace McpCodeReviewServer.Rules.Abstractions;
public interface ICodeRule
{
ReviewIssue? Evaluate(RuleContext context);
}
Single responsibility: One rule checks one thing
Immutable: Takes a RuleContext, returns a finding or null
One finding per rule: Each rule creates only one finding. If a rule matches many times, only the first match is reported.
The Provider Interface: IRuleGroupProvider
Rules are organized into groups (categories). Each group has a provider.
Create Rules/Abstractions/IRuleGroupProvider.cs:
namespace McpCodeReviewServer.Rules.Abstractions;
public interface IRuleGroupProvider
{
string Category { get; }
IReadOnlyCollection<ICodeRule> BuildRules();
}
Category: is name, stable identifier for grouping (e.g., "async correctness", "security")
BuildRules: Factory method that returns configured rule instances
Called once at startup: Providers build their rules during DI registration, not on every analysis
Concrete Rule Type 1: ContainsTokenRule
The simplest rule type: check if a specific token appears in code.
Create Rules/Abstractions/ContainsTokenRule.cs:
using McpCodeReviewServer.Models;
namespace McpCodeReviewServer.Rules.Abstractions;
public sealed class ContainsTokenRule : ICodeRule
{
private readonly string _token;
private readonly string _severity;
private readonly string _category;
private readonly string _description;
private readonly string _fix;
public ContainsTokenRule(
string token,
string severity,
string category,
string description,
string fix)
{
_token = token;
_severity = severity;
_category = category;
_description = description;
_fix = fix;
}
public ReviewIssue? Evaluate(RuleContext context)
{
// Search for token in each line
var line = FindFirstLine(context.Lines, _token);
if (line is null)
{
// Token not found
return null;
}
// Token found, emit issue with line number
return new ReviewIssue(
_severity,
_category,
line, // Line number (1-indexed)
_description,
_fix
);
}
private static int? FindFirstLine(IReadOnlyList<string> lines, string token)
{
for (var i = 0; i < lines.Count; i++)
{
if (lines[i].Contains(token, StringComparison.Ordinal))
{
return i + 1; // Line numbers are 1-indexed for humans
}
}
return null; // Not found
}
}
Example Usage
// Create a rule to detect async void methods
var asyncVoidRule = new ContainsTokenRule(
token: "async void ",
severity: "critical",
category: "async correctness",
description: "Avoid async void methods; exceptions can be unobserved.",
fix: "Return Task instead. Example: public async Task MyMethodAsync() { }"
);
// Evaluate it
var context = new RuleContext("public async void BadMethod() { }", ...);
var issue = asyncVoidRule.Evaluate(context);
// Result: ReviewIssue with severity="critical", line=1, description=..., fix=...
When to use ContainsTokenRule:
Simple substring detection
Tokens that don't need complex pattern matching
Examples: "async void ", ".Result", ".Wait("
Concrete Rule Type 2: RegexRule
For more complex patterns, use regex.
Create Rules/Abstractions/RegexRule.cs:
using McpCodeReviewServer.Models;
using System.Text.RegularExpressions;
namespace McpCodeReviewServer.Rules.Abstractions;
public sealed class RegexRule : ICodeRule
{
private readonly Regex _regex;
private readonly string _severity;
private readonly string _category;
private readonly string _description;
private readonly string _fix;
public RegexRule(
Regex regex,
string severity,
string category,
string description,
string fix)
{
_regex = regex;
_severity = severity;
_category = category;
_description = description;
_fix = fix;
}
public ReviewIssue? Evaluate(RuleContext context)
{
// Search for pattern in each line
for (var i = 0; i < context.Lines.Count; i++)
{
if (_regex.IsMatch(context.Lines[i]))
{
// Pattern matched on this line
return new ReviewIssue(
_severity,
_category,
i + 1, // Line number (1-indexed)
_description,
_fix
);
}
}
return null; // No match
}
}
Example Usage
// Create a rule to detect hardcoded API keys (basic example)
var apiKeyRule = new RegexRule(
regex: new Regex(@"['\"]sk_[a-zA-Z0-9_]{20,}['\"]", RegexOptions.IgnoreCase),
severity: "critical",
category: "security",
description: "Hardcoded API key detected.",
fix: "Remove hardcoded keys. Use environment variables or secure configuration."
);
var context = new RuleContext(@"var token = ""sk_prod_abc123def456""; // Bad!", ...);
var issue = apiKeyRule.Evaluate(context);
// Result: ReviewIssue with severity="critical", line=1, description=...
When to use RegexRule:
Complex patterns
Multi-token detection
Variations and optional parts
Examples: Method signatures, security patterns, naming violations
Building a Rule Provider: AsyncRulesProvider Example
Now let's build a complete provider with multiple related rules.
Create Rules/Async/AsyncRulesProvider.cs:
using McpCodeReviewServer.Rules.Abstractions;
namespace McpCodeReviewServer.Rules.Async;
public sealed class AsyncRulesProvider : IRuleGroupProvider
{
/// <inheritdoc/>
public string Category => "async correctness";
/// <inheritdoc/>
public IReadOnlyCollection<ICodeRule> BuildRules() =>
new ICodeRule[]
{
// Rule 1: Detect async void (except event handlers)
new ContainsTokenRule(
token: "async void ",
severity: "critical",
category: "async correctness",
description: "Avoid async void methods except event handlers; exceptions can be unobserved and crash the process.",
fix: "Return Task instead of async void. Example: public async Task MyMethodAsync() { ... }"),
// Rule 2: Detect .Result blocking
new ContainsTokenRule(
token: ".Result",
severity: "warning",
category: "async correctness",
description: "Synchronous blocking on Task via .Result can deadlock and hurts scalability.",
fix: "Use await instead of .Result. Propagate async through the call chain."),
// Rule 3: Detect .Wait() blocking
new ContainsTokenRule(
token: ".Wait(",
severity: "warning",
category: "async correctness",
description: "Blocking wait on asynchronous work can deadlock and waste thread pool threads.",
fix: "Use await task.ConfigureAwait(false) in library code, or await task in app code.")
};
}
Category property: Returns "async correctness" (used for grouping and scoring)
BuildRules method: Returns array of configured rules
Mix of rules: All three use ContainsTokenRule (simple token detection fits these checks)
Severity levels: Critical for async void, warning for blocking operations
Clear fixes: Each issue includes actionable advice
How This Integrates
When ReviewAnalyzer is initialized:
// Program.cs
builder.Services.AddCodeReviewEngine();
// This registers AsyncRulesProvider as IRuleGroupProvider
When ReviewAnalyzer starts:
// ReviewAnalyzer constructor
public ReviewAnalyzer(IEnumerable<IRuleGroupProvider> ruleGroups)
{
// DI gives us all 8 providers (including AsyncRulesProvider)
_rules = ruleGroups
.SelectMany(group =>
group.BuildRules() // AsyncRulesProvider.BuildRules() returns 3 rules
.Select(rule => new RegisteredRule(group.Category, rule))
)
.ToArray();
// Now _rules contains ~127 rules total from all providers
}
When code is analyzed:
foreach (var rule in _rules)
{
// For each of the 3 async rules, evaluate them
var issue = rule.Evaluate(context);
// If async void found, emit finding
}
All Eight Rule Categories
Here's a quick overview of what each category checks:
1. Async Correctness
async void methods
.Result blocking
.Wait() blocking
Other deadlock patterns
2. Security
Hardcoded passwords/keys
SQL injection patterns
Unsafe deserialization
Missing input validation
3. Performance
4. Maintainability
Method length violations
Excessive parameters
Complex conditionals
Poor naming conventions
5. Method Design
6. Type Design
7. File and Folder
Naming conventions for files
Folder organization violations
File too large
Related functionality in wrong locations
8. C# Modernization
Using old patterns instead of C# 10+ features
Not using records when appropriate
Old nullable reference handling
Legacy LINQ patterns
Each has a provider file in Rules/<Category>/.
Creating a New Rule Provider
Let's say you want to add a "Naming Conventions" category. Here's the pattern:
Create Rules/NamingConventions/NamingConventionsRulesProvider.cs:
using McpCodeReviewServer.Rules.Abstractions;
using System.Text.RegularExpressions;
namespace McpCodeReviewServer.Rules.NamingConventions;
public sealed class NamingConventionsRulesProvider : IRuleGroupProvider
{
public string Category => "naming conventions";
public IReadOnlyCollection<ICodeRule> BuildRules() =>
new ICodeRule[]
{
// Rule: Variables should be camelCase, not snake_case
new RegexRule(
regex: new Regex(@"\b[a-z]+_[a-z]+\s*=", RegexOptions.IgnoreCase),
severity: "suggestion",
category: "naming conventions",
description: "Variable name uses snake_case; C# convention is camelCase.",
fix: "Rename to camelCase: myVariable instead of my_variable."),
// Rule: Constants should be UPPER_SNAKE_CASE
new RegexRule(
regex: new Regex(@"const\s+\w+\s+[a-z][a-zA-Z0-9]*\s*=", RegexOptions.IgnoreCase),
severity: "suggestion",
category: "naming conventions",
description: "Constant name should be UPPER_SNAKE_CASE.",
fix: "Rename to UPPER_SNAKE_CASE: MAX_RETRIES instead of maxRetries."),
// Rule: Private fields should start with underscore
new RegexRule(
regex: new Regex(@"private\s+\w+\s+(?!_)[a-z]", RegexOptions.IgnoreCase),
severity: "suggestion",
category: "naming conventions",
description: "Private field should start with underscore.",
fix: "Rename to _fieldName instead of fieldName."),
};
}
Then register it in ServiceCollectionExtensions.cs:
// Add to AddCodeReviewEngine method
services.AddSingleton<IRuleGroupProvider, NamingConventionsRulesProvider>();
That's it. Now your new category is part of the review engine. No changes needed elsewhere.
Creating a Custom Rule Type
Sometimes token and regex aren't enough. Use DelegateRule for custom logic.
Create Rules/Abstractions/DelegateRule.cs:
using McpCodeReviewServer.Models;
namespace McpCodeReviewServer.Rules.Abstractions;
public sealed class DelegateRule : ICodeRule
{
private readonly Func<RuleContext, ReviewIssue?> _evaluate;
private readonly string _severity;
private readonly string _category;
public DelegateRule( string severity, string category, Func<RuleContext, ReviewIssue?> evaluateFunc)
{
_severity = severity;
_category = category;
_evaluate = evaluateFunc;
}
public ReviewIssue? Evaluate(RuleContext context) => _evaluate(context);
}
Example: Complex Logic
// Detect if method has cyclomatic complexity > 10
new DelegateRule(
severity: "warning",
category: "maintainability",
evaluateFunc: (context) =>
{
// Custom logic: count conditional branches
var branchCount = context.Code.Count(c => c == 'if' || c == 'for' || c == 'while' || c == 'switch');
if (branchCount > 10)
{
return new ReviewIssue(
"warning",
"maintainability",
null,
"Method complexity exceeds recommended threshold.",
"Refactor to reduce branching or extract logic."
);
}
return null;
}
)
Extending Via Markdown (No Code Required here)
For proposed future rules that don't require code changes, edit markdown files.
Create documentation/rule-catalog/async-rules.md:
# Async Correctness Rules
## Existing Rules
- Rule: Avoid async void (except event handlers)
- Rule: Detect .Result blocking
- Rule: Detect .Wait() blocking
## Add New Rules
- Rule name: Detect ConfigureAwait missing; Category: async correctness; Severity: warning; Detection: Look for "await" without ".ConfigureAwait"; Fix: Add ".ConfigureAwait(false)" in library code
- Rule name: Detect Task.Run in async context; Category: async correctness; Severity: warning; Detection: Regex pattern; Fix: Use native async methods when available
The get_rule_backlog tool reads these entries and returns them as JSON. Teams can propose rules without coding.
Summary
Now you understand the complete rule system:
ICodeRule: The interface—all rules return ReviewIssue?
IRuleGroupProvider: Organizes rules into categories
Rule types:
ContainsTokenRule: Simple substring detection
RegexRule: Complex pattern matching
DelegateRule: Custom logic
Building providers: Group related rules, implement the interface
Registration: Add to DI in ServiceCollectionExtensions.cs
Extension: New rules don't require touching existing code
Backlog: Propose rules via markdown
The system is extensible: To add a new rule, create a provider or add to an existing one. To add a new category, create a new provider and register it.
Ready for next Article? Let's ship it.
The cat is neither alive nor dead and honestly, that's the most exciting place to be. There are a lot more layers to uncover.
Explore the entire series Schrodingers-AI
I’d suggest cloning the code from my repository: review-my-code-mcp
It’ll make it easier to follow along with the project as we build it. That said, it’s not strictly required since we’ll be building everything step by step throughout the series.
Previous: Part 14.2: ReviewMyCode MCP Server: Core Implementation
Next: Part 14.4: ReviewMyCode MCP Server: Deployment, Docker, and Testing