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:
14.2 ReviewMyCode MCP Server: Core Implementation
14.4 ReviewMyCode MCP Server: Deployment, Docker, and Testing
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.

Part 14.2: ReviewMyCode MCP Server: Core Implementation
In Part 14.1, we covered the project structure and organization
How
Program.csbootstraps the MCP server and registers servicesThe MCP request/response flow
How dependency injection wires everything together
Now we dive into the core execution engine: the services that actually do the work.
When a client calls review_csharp_code with C# code, two services take action:
ReviewAnalyzer: Executes all rules against the code and collects findings
ReviewScorer: Calculates a 0-10 quality score and per-category scores
This article explains both, with code, flow diagrams, and step-by-step examples so you see exactly how findings flow through the system.
The Pipeline Overview

Let's implement each layer.
The Interfaces: Defining the Contracts
Before implementation, we define interfaces so services are testable and replaceable.
IReviewAnalyzer
Create Services/IReviewAnalyzer.cs:
using McpCodeReviewServer.Models;
namespace McpCodeReviewServer.Services;
public interface IReviewAnalyzer
{
ReviewAnalysisResult Analyze(string code, int maxIssues);
}code: The raw C# source (string)maxIssues: Client can request how many issues to get back (we check more, but cap the response)Return:
ReviewAnalysisResultcontains:All findings (used for scoring)
Returned findings (capped by maxIssues)
Category coverage (how many rules checked/matched per category)
IReviewScorer
Create Services/IReviewScorer.cs:
using McpCodeReviewServer.Models;
namespace McpCodeReviewServer.Services;
public interface IReviewScorer
{
int CalculateScore(IReadOnlyCollection<ReviewIssue> issues);
IReadOnlyCollection<CategoryReviewScore> CalculateCategoryScores( IReadOnlyCollection<CategoryAnalysis> categoryAnalyses,
IReadOnlyCollection<ReviewIssue> issues);
}CalculateScore: Takes issues, returns 0-10 scoreCalculateCategoryScores: Breaks scores down by category with rule coverage metrics
The Data Container: RuleContext
Before analyzing code, we wrap it in a data structure that rules can use.
Create Rules/Abstractions/RuleContext.cs:
namespace McpCodeReviewServer.Rules.Abstractions;
public sealed class RuleContext
{
public RuleContext(string code, IReadOnlyList<string> lines)
{
Code = code;
Lines = lines;
}
public string Code { get; }
public IReadOnlyList<string> Lines { get; }
}Rules need both the full code (for regex, pattern matching) and line-by-line access (for line numbers)
Normalization ensures consistent line breaks across Windows (
\r\n) and Unix (\n) systemsImmutable design means no rule can accidentally corrupt data
Implementation 1: ReviewAnalyzer
Now let's implement the analyzer that runs all rules.
Create Services/ReviewAnalyzer.cs:
using McpCodeReviewServer.Models;
using McpCodeReviewServer.Rules.Abstractions;
namespace McpCodeReviewServer.Services;
public sealed class ReviewAnalyzer : IReviewAnalyzer
{
// All rules collected at startup (from all registered providers)
private readonly IReadOnlyCollection<RegisteredRule> _rules;
public ReviewAnalyzer(IEnumerable<IRuleGroupProvider> ruleGroups)
{
// Build the master rule list from all providers
_rules = ruleGroups
.SelectMany(group =>
group.BuildRules() // Each provider returns its rules
.Select(rule => new RegisteredRule(group.Category, rule)) // Pair each rule with its category
)
.ToArray();
}
public ReviewAnalysisResult Analyze(string code, int maxIssues)
{
// Ensure maxIssues is at least 1
var normalizedMax = Math.Max(1, maxIssues);
// Step 1: Normalize code into lines
// This handles Windows (\r\n) and Unix (\n) line endings consistently
var lines = NormalizeLines(code);
// Step 2: Create context (immutable container for rules to use)
var context = new RuleContext(code, lines);
// Step 3: Prepare to collect findings
var allIssues = new List<ReviewIssue>();
var categoryCoverage = new Dictionary<string, CategoryCounter>(StringComparer.OrdinalIgnoreCase);
// Step 4: Run each rule
foreach (var rule in _rules)
{
// Get or create category entry
var category = string.IsNullOrWhiteSpace(rule.Category) ? "uncategorized" : rule.Category;
if (!categoryCoverage.TryGetValue(category, out var counter))
{
counter = new CategoryCounter();
categoryCoverage[category] = counter;
}
// Mark this rule as checked
counter.RulesChecked++;
// Evaluate the rule against the context
// Rule returns ReviewIssue or null
var issue = rule.Evaluate(context);
// If rule matched, record the finding
if (issue is not null)
{
counter.RulesMatched++;
allIssues.Add(issue);
}
}
// Step 5: Cap returned issues to maxIssues
// We check all rules, but only return up to maxIssues
var returnedIssues = allIssues.Take(normalizedMax).ToArray();
// Step 6: Build category analysis
var categoryAnalyses = categoryCoverage
.Select(entry => new CategoryAnalysis(
entry.Key, // Category name
entry.Value.RulesChecked, // How many rules in this category
entry.Value.RulesMatched)) // How many matched
.OrderBy(entry => entry.Category, StringComparer.OrdinalIgnoreCase)
.ToArray();
// Return complete result
return new ReviewAnalysisResult(allIssues, returnedIssues, categoryAnalyses);
}
private static IReadOnlyList<string> NormalizeLines(string code)
{
return code
.Replace("\r\n", "\n", StringComparison.Ordinal) // Windows → Unix
.Replace('\r', '\n') // Old Mac → Unix
.Split('\n');
}
private sealed class RegisteredRule
{
public RegisteredRule(string category, ICodeRule rule)
{
Category = category;
Rule = rule;
}
public string Category { get; }
public ICodeRule Rule { get; }
public ReviewIssue? Evaluate(RuleContext context) => Rule.Evaluate(context);
}
private sealed class CategoryCounter
{
public int RulesChecked { get; set; }
public int RulesMatched { get; set; }
}
}Walk-Through
Let's trace through an example. Suppose we call:
var analyzer = /* ReviewAnalyzer instance */;
var result = analyzer.Analyze(
code: "public async void BadAsync() { }",
maxIssues: 50
);This is how it looks in cursor:







Emanuel MartinsPosted May 12, 2026, 5:58 PM
The link to Article 14.1 is not working in any of the other articles. ;-;]