DEV Community

Prashant Patil
Prashant Patil

Posted on

How I Stopped Claude From Hallucinating NuGet Package APIs With This custom MCP Server

TL;DR: Built an MCP server that gives Claude real-time access to any NuGet package's actual API surface by extracting assembly metadata on-demand. No more hallucinated method names, wrong signatures, or outdated examples.


The Problem: AI Hallucinations Cost Developer Time

You know this scenario:

You: "Hey Claude, show me how to use the OpenAI SDK v2.1.0"

Claude: "Sure! Use OpenAI.CreateCompletion() like this..."

You: Copy-paste code → Compile error

Reality: That method doesn't exist in v2.1.0. The API changed months ago, but Claude's training data is frozen at January 2025.

Every. Single. Day. Developers waste hours debugging code that looked correct but was based on outdated or hallucinated API knowledge.

The core issue: NuGet packages update constantly. Claude's training data doesn't.


The Solution: Ground Truth via MCP

I built an MCP (Model Context Protocol) server that:

  1. Downloads any NuGet package on-demand
  2. Extracts complete assembly metadata using System.Reflection.MetadataLoadContext
  3. Returns the actual API surface - every type, method, property, signature, XML doc
  4. Cleans up automatically - no leftover temp files

When Claude needs to generate code against a NuGet package, it calls my server and gets ground truth instead of guessing.


How It Works

Architecture

┌─────────────┐         ┌──────────────┐         ┌─────────────┐
│   Claude    │  MCP    │  MCP Server  │  NuGet  │  NuGet.org  │
│   Desktop   │◄───────►│  (ASP.NET)   │◄───────►│  Repository │
└─────────────┘         └──────────────┘         └─────────────┘
                               │
                               │ Reflection
                               ▼
                        ┌──────────────┐
                        │   Assembly    │
                        │   Metadata    │
                        │  Extraction   │
                        └──────────────┘
Enter fullscreen mode Exit fullscreen mode

Three Core Operations

1. Search Packages

[McpServerTool]
[Description("Searches NuGet.org for packages matching query terms")]
public async Task<string> SearchNuGetPackages(
    string query,
    int take = 20)
{
    var results = await _nugetService.SearchPackagesAsync(query, take);
    return JsonSerializer.Serialize(results, _jsonOptions);
}
Enter fullscreen mode Exit fullscreen mode

2. Get Package Info

[McpServerTool]
[Description("Retrieves package metadata: description, authors, dependencies, etc.")]
public async Task<string> GetNuGetPackageInfo(
    string packageId,
    string? version = null)
{
    var info = await _nugetService.GetPackageInfoAsync(packageId, version);
    return JsonSerializer.Serialize(info, _jsonOptions);
}
Enter fullscreen mode Exit fullscreen mode

3. Explore Package API (The Magic)

[McpServerTool]
[Description("Extracts complete assembly metadata - all public types, methods, properties, signatures")]
public async Task<string> ExploreNuGetPackage(
    string packageId,
    string? version = null,
    string? targetFramework = null)
{
    var result = await _nugetService.ExplorePackageAsync(
        packageId, version, targetFramework);
    return JsonSerializer.Serialize(result, _jsonOptions);
}
Enter fullscreen mode Exit fullscreen mode

The Real Magic: Safe Assembly Analysis

The tricky part isn't downloading packages - it's analyzing assemblies without executing code.

Here's the approach:

private AssemblyMetadata AnalyzePublicApiOnly(string dllPath, string targetFramework)
{
    // Build resolver with system assemblies
    var paths = new List<string> { dllPath, typeof(object).Assembly.Location };

    var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory();
    if (Directory.Exists(runtimeDir))
    {
        paths.AddRange(Directory.GetFiles(runtimeDir, "*.dll"));
    }

    var resolver = new PathAssemblyResolver(paths.Distinct().ToList());
    using var mlc = new MetadataLoadContext(resolver, typeof(object).Assembly.GetName().Name);

    var assembly = mlc.LoadFromAssemblyPath(dllPath);

    // Extract all public types
    var publicTypes = assembly.GetTypes()
        .Where(t => t.IsPublic && !t.IsNested)
        .OrderBy(t => t.Namespace)
        .ThenBy(t => t.Name)
        .ToList();

    // Generate markdown documentation
    var sb = new StringBuilder();
    sb.AppendLine($"# {assembly.GetName().Name} v{assembly.GetName().Version}");

    foreach (var type in publicTypes)
    {
        GenerateCompactTypeSignature(sb, type);
    }

    return new AssemblyMetadata
    {
        AssemblyName = assembly.GetName().Name,
        Version = assembly.GetName().Version.ToString(),
        MarkdownDocumentation = sb.ToString(),
        TotalTypes = publicTypes.Count
    };
}
Enter fullscreen mode Exit fullscreen mode

Key insight: MetadataLoadContext lets you inspect assemblies without loading them into the main execution context. No code execution, no security risks, no version conflicts.


Production-Ready Features

1. Concurrency Control

private readonly SemaphoreSlim _concurrencyLimiter;

public NuGetService(...)
{
    _concurrencyLimiter = new SemaphoreSlim(_config.MaxConcurrentOperations);
}

public async Task<PackageAnalysisResult> ExplorePackageAsync(...)
{
    if (!await _concurrencyLimiter.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken))
    {
        throw new ServiceCapacityException("Service at capacity. Retry in a moment.");
    }

    try
    {
        // Analysis logic
    }
    finally
    {
        _concurrencyLimiter.Release();
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Automatic Cleanup

private async Task<PackageAnalysisResult> ExplorePackageInternalAsync(...)
{
    string? tempPath = null;

    try
    {
        tempPath = await DownloadAndExtractPackageAsync(...);
        // Analyze assemblies
        return result;
    }
    finally
    {
        if (_config.EnableTempCleanup && !string.IsNullOrEmpty(tempPath))
        {
            CleanupTempDirectory(tempPath);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Retry Logic with Exponential Backoff

private async Task<NuGetVersion> ResolveVersionWithRetryAsync(...)
{
    var retryCount = 0;
    var delay = _config.Retry.InitialDelay;

    while (true)
    {
        try
        {
            return await ResolveVersionAsync(...);
        }
        catch (Exception ex) when (IsTransientError(ex) && retryCount < _config.Retry.MaxRetries)
        {
            retryCount++;
            await Task.Delay(delay, cancellationToken);
            delay = TimeSpan.FromMilliseconds(
                Math.Min(delay.TotalMilliseconds * 2, _config.Retry.MaxDelay.TotalMilliseconds));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Size Limits & Validation

{
  "NuGetService": {
    "MaxPackageSizeBytes": 104857600,
    "MaxConcurrentOperations": 5,
    "OperationTimeout": "00:02:00",
    "MaxAssembliesPerPackage": 50,
    "DownloadTimeout": "00:00:30"
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Health Checks

public class NuGetServiceHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(...)
    {
        try
        {
            var results = await _service.SearchPackagesAsync("Newtonsoft.Json", take: 1);
            return results.Count > 0
                ? HealthCheckResult.Healthy("NuGet.org accessible")
                : HealthCheckResult.Degraded("No results from NuGet.org");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Service unavailable", ex);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Impact

Before:

User: "How do I use Polly 8.0's retry policy?"

Claude: "Use PolicyBuilder.RetryAsync(3)..." ❌
→ Method signature changed in v8.0
→ User gets compile errors
→ 20 minutes wasted debugging
Enter fullscreen mode Exit fullscreen mode

After:

User: "How do I use Polly 8.0's retry policy?"

Claude: [calls explore_nuget_package("Polly", "8.0.0")]
→ Gets actual API surface from DLL
→ Sees ResiliencePipeline<T> is the new pattern
→ Generates correct code ✅

User: Copy-paste → Compiles immediately
Enter fullscreen mode Exit fullscreen mode

Tech Stack

  • ASP.NET Core 10.0 - MCP server host
  • ModelContextProtocol SDK - MCP implementation
  • NuGet.Protocol - Package download/search
  • System.Reflection.MetadataLoadContext - Safe assembly inspection
  • Docker - Easy deployment

Configuration

{
  "NuGetService": {
    "MaxPackageSizeBytes": 104857600,
    "MaxConcurrentOperations": 5,
    "OperationTimeout": "00:02:00",
    "SupportedFrameworks": [
      "net8.0", "net7.0", "net6.0", 
      "netstandard2.1", "netstandard2.0"
    ],
    "FrameworkPriority": ["net8.0", "net7.0", "net6.0"],
    "EnableTempCleanup": true,
    "Retry": {
      "MaxRetries": 3,
      "InitialDelay": "00:00:00.100",
      "MaxDelay": "00:00:05"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Deployment

Docker Compose

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "5020:5020"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:5020
Enter fullscreen mode Exit fullscreen mode

Run Locally

docker-compose build
docker-compose up -d
docker-compose logs -f
Enter fullscreen mode Exit fullscreen mode

Example: Exploring OpenAI SDK

// Claude calls: explore_nuget_package("OpenAI", "2.1.0", "net8.0")

// Returns:
{
  "packageId": "OpenAI",
  "version": "2.1.0",
  "assemblies": [
    {
      "assemblyName": "OpenAI",
      "version": "2.1.0.0",
      "markdownDocumentation": "# OpenAI v2.1.0.0\n\n**144 public types**\n\n## OpenAI.Chat\n\n**ChatClient** `public class`\n*5 ctor • 12 prop • 23 method*\n```

csharp\nTask<ChatCompletion> CompleteChatAsync(IEnumerable<ChatMessage> messages, ChatCompletionOptions options)\nChatCompletion CompleteChat(IEnumerable<ChatMessage> messages)\n...\n

```\n...",
      "totalTypes": 144
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Claude now has:

  • ✅ Exact class names
  • ✅ Correct method signatures
  • ✅ Parameter types and names
  • ✅ Return types
  • ✅ Overload variations

Result: Generated code compiles on first try.


## Current Limitations:

  • Only analyzes .NET assemblies (no Python/npm packages)
  • Markdown output only (could add JSON schema export)
  • No caching layer yet (downloads every time)
  • Single-framework analysis per request

Why This Matters

Every NuGet package update creates a hallucination risk.

  • Azure SDK updates monthly
  • AWS SDK updates weekly
  • OpenAI SDK breaking changes every major version
  • Entity Framework, ASP.NET, Polly - all evolving constantly

Traditional solutions don't scale:

  • Documentation lags behind releases
  • IntelliSense requires IDE context
  • GitHub browsing is manual and slow
  • Training data becomes stale instantly

MCP solves this elegantly:

  • Real-time, on-demand metadata extraction
  • Works with ANY .NET package
  • Zero manual maintenance
  • Seamless Claude integration

Configure Claude:

For public access (Claude.ai web): Use ngrok: ngrok http 5020 then configure Claude with custom connector using your ngrok HTTPS URL.

Test it:

Ask Claude: "What methods are available in Polly 8.0?"
Watch it call your MCP server and get real data!
Enter fullscreen mode Exit fullscreen mode

Conclusion

AI hallucinations aren't a model problem - they're a data freshness problem.

Instead of waiting for the next training run, we can give AI models real-time access to ground truth.

MCP makes this trivial. My NuGet explorer proves it works.

What package ecosystem should I tackle next? npm? PyPI? Maven? Let me know in the comments!


Tech Details

Dependencies:

  • ModelContextProtocol 0.5.0-preview.1
  • NuGet.Protocol 7.0.1
  • System.Reflection.MetadataLoadContext 10.0.1

Performance:

  • Average exploration: ~2-4 seconds
  • Newtonsoft.Json (144 types): 2.1s
  • Entity Framework Core (800+ types): 5.3s

Safety:

  • No code execution
  • Sandboxed analysis
  • Size limits enforced
  • Automatic cleanup

Questions? Feedback? Hit me in the comments! 👇

Top comments (0)