AI  

Schrödinger's AI Part 14.3: ReviewMyCode MCP Server: Rules & Extensibility

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:

  1. ICodeRule: The interface all rules implement

  2. IRuleGroupProvider: How to organize rules by category

  3. Concrete rule types: Token detection, regex, custom delegates

  4. Building a provider: Grouping related rules

  5. All eight categories: What each checks

  6. 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.")
        };
}
  1. Category property: Returns "async correctness" (used for grouping and scoring)

  2. BuildRules method: Returns array of configured rules

  3. Mix of rules: All three use ContainsTokenRule (simple token detection fits these checks)

  4. Severity levels: Critical for async void, warning for blocking operations

  5. 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

  • Unnecessary boxing

  • LINQ inefficiencies

  • String concatenation in loops

  • Other performance antipatterns

4. Maintainability

  • Method length violations

  • Excessive parameters

  • Complex conditionals

  • Poor naming conventions

5. Method Design

  • Methods doing too much

  • Violation of single responsibility principle

  • Parameter count issues

6. Type Design

  • Classes/interfaces violating design principles

  • Inappropriate access modifiers

  • Inheritance misuse

  • Record design issues

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:

  1. ICodeRule: The interface—all rules return ReviewIssue?

  2. IRuleGroupProvider: Organizes rules into categories

  3. Rule types:

    • ContainsTokenRule: Simple substring detection

    • RegexRule: Complex pattern matching

    • DelegateRule: Custom logic

  4. Building providers: Group related rules, implement the interface

  5. Registration: Add to DI in ServiceCollectionExtensions.cs

  6. Extension: New rules don't require touching existing code

  7. 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