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:
- Downloads any NuGet package on-demand
-
Extracts complete assembly metadata using
System.Reflection.MetadataLoadContext - Returns the actual API surface - every type, method, property, signature, XML doc
- 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 │
└──────────────┘
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);
}
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);
}
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);
}
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
};
}
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();
}
}
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);
}
}
}
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));
}
}
}
4. Size Limits & Validation
{
"NuGetService": {
"MaxPackageSizeBytes": 104857600,
"MaxConcurrentOperations": 5,
"OperationTimeout": "00:02:00",
"MaxAssembliesPerPackage": 50,
"DownloadTimeout": "00:00:30"
}
}
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);
}
}
}
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
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
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"
}
}
}
Deployment
Docker Compose
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "5020:5020"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:5020
Run Locally
docker-compose build
docker-compose up -d
docker-compose logs -f
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
}
]
}
Claude now has:
- ✅ Exact class names
- ✅ Correct method signatures
- ✅ Parameter types and names
- ✅ Return types
- ✅ Overload variations
Result: Generated code compiles on first try.
- 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!
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)