Skip to content

Repository files navigation

dotnet-diagnostics-mcp

CI

An MCP server for LLM-driven performance diagnostics on .NET applications — primarily targeting .NET 10, with .NET 8/9 target processes also officially supported and validated in CI (see docs/runtime-version-compat-matrix.md). Normal EventPipe and ClrMD diagnostics require no target code changes or prior instrumentation. The explicit exception is collect_sample(kind="method-params"), an opt-in, privileged, security-gated dynamic profiler attach that temporarily instruments an allowlist of methods (requires the target to be .NET 8+).

Status: 17 unified tools in the full surface (13 default plus 4 configuration-gated), HTTP + stdio transports, IoT-style triage (6+ steps → 2 steps). See docs/ for full reference.

Three ways to use it

This repo ships three NuGet packages built on the same Core diagnostics engine — pick by who (or what) is driving:

Package Driver Surface Docs
dotnet-diagnostics-mcp An LLM, via an MCP client MCP tools over HTTP (bearer) or stdio this README + docs/
dotnet-diagnostics-cli A human / script / CI Sub-commands + a stateful session REPL (no HTTP, no bearer, no daemon) docs/cli-reference.md
dotnet-diagnostics-benchmarkdotnet BenchmarkDotNet, via [DotnetDiagnosticsDiagnoser] In-process IDiagnoser that attaches Core captures to a [Benchmark] and emits a "biggest offenders" report src/DotnetDiagnostics.BenchmarkDotNet/README.md

Most of this README is about the MCP server. If you want to run diagnostics yourself, jump to the Standalone CLI section or the CLI reference. If you want to attribute why a benchmark is slow or allocates, jump to the BenchmarkDotNet diagnoser README.

MCP Server Start Here

Choose the onboarding track before reading the rest of the docs:

Track Choose this when First steps Continue with
Local dev (--stdio) Your MCP client runs on the same machine and can spawn the server itself No daemon, no bearer token, no ports docs/client-setup.md → Option A
Sidecar / shared deploy (HTTP + bearer) You want one long-running MCP server for Docker, Kubernetes, or multiple clients Configure credentials up front (MCP_BEARER_TOKEN, Auth__BearerTokens__*, or OIDC), then start the HTTP server docs/client-setup.md → Option B, docs/local-docker-sidecar.md, deploy/k8s/README.md, docs/consumer-install.md → Linux sidecar checklist

If an HTTP tool call later fails with PermissionDenied or ServerNotAvailableException: Permission denied, run inspect_process(view="preflight") first; the troubleshooting guides below link back to the same remediation-first check.


Table of Contents


Quick Start

One call to understand your app's health:

# MCP call
inspect_process(view="triage")

Response excerpt (rationales shortened):

{
  "modelVersion": 2,
  "assessment": "critical",
  "severity": "Critical",
  "observedSignals": [
    {
      "name": "threadpool.queue",
      "level": "critical",
      "summary": "The ThreadPool queue contained 1191 work items.",
      "evidence": [
        {"name": "threadpool-queue-length", "value": 1191, "comparison": ">=", "threshold": 200, "unit": "items", "rationale": "Queue crossed the critical threshold."}
      ]
    }
  ],
  "hypotheses": [
    {
      "name": "threadpool.backlog",
      "confidence": "moderate",
      "summary": "Work was queued faster than the ThreadPool completed it; counters do not prove starvation.",
      "supportingEvidence": [{"name": "threadpool-queue-length", "value": 1191, "comparison": ">=", "threshold": 50, "rationale": "Large queue supports a backlog hypothesis."}],
      "contradictingEvidence": [],
      "nextStep": "Collect ThreadPool events and blocking stacks to distinguish sustained starvation, blocking, and transient demand."
    }
  ],
  "topIndicators": [
    {"name": "threadpool-queue-length", "value": 1191, "score": 100, "level": "critical"}
  ],
  // deprecated — kept for compatibility, scheduled for removal in v1.0; prefer topIndicators
  "verdict": "threadpool-starvation",
  "secondaryVerdicts": null
}

observedSignals report threshold crossings; hypotheses explain bounded interpretations and the evidence needed to confirm them. A low-CPU snapshot with a small queue is inconclusive, not categorically io-bound. verdict / secondaryVerdicts remain for compatibility and are deprecated for removal in v1.0. TopIndicators remain available on every result.


Install

Three distributions — pick by environment. Full walkthrough: docs/consumer-install.md

# .NET global tool (requires .NET 10 SDK)
dotnet tool install -g dotnet-diagnostics-mcp
export MCP_BEARER_TOKEN="$(openssl rand -hex 32)"
dotnet-diagnostics-mcp --urls http://127.0.0.1:8787
# Loopback-only alternative: omit MCP_BEARER_TOKEN and copy the generated ephemeral token from the startup warning.

# Container — host-loopback only, local dev (container binds 0.0.0.0:8080 internally)
# MCP_ALLOW_INSECURE_HTTP=true is required for cleartext on the container-internal non-loopback bind;
# -p 127.0.0.1:8787:8080 restricts host access to loopback. Use TLS for production.
docker run -d -p 127.0.0.1:8787:8080 \
  -e MCP_BEARER_TOKEN=$(openssl rand -hex 32) \
  -e MCP_ALLOW_INSECURE_HTTP=true \
  ghcr.io/pedrosakuma/dotnet-diagnostics:latest

# Self-contained binary — see Releases page

The generated ephemeral-token fallback applies only to loopback/local HTTP. Non-loopback deployments (including containers/sidecars that bind 0.0.0.0) must configure MCP_BEARER_TOKEN, Auth__BearerTokens__*, or OIDC before startup.

Transport options
Transport Use case Auth
stdio Local dev (Copilot CLI, Claude Desktop) None (OS-level trust)
HTTP loopback Single-host / dev, bind to http://127.0.0.1:<port> Bearer token
HTTP + TLS Sidecar, shared host: direct PEM TLS (MCP_TLS_CERTIFICATE_PEM) or trusted proxy (MCP_TRUSTED_PROXY_CIDRS) Bearer token

Non-loopback cleartext HTTP is refused by default. See docs/client-setup.md → Transport security.

Linux ptrace note

Most diagnostics, including EventPipe collectors, need no kernel ptrace permission. ClrMD live-memory readers are different: on Debian/Ubuntu/WSL, kernel.yama.ptrace_scope=1 blocks same-UID peer attach. For Docker or Kubernetes, grant CAP_SYS_PTRACE only to the diagnostics sidecar (--cap-add SYS_PTRACE / securityContext.capabilities.add) rather than weakening the host.

On a bare host, prefer the CLI's --launch descendant attach for an app you can start, offline dump analysis, or EventPipe collectors. The fallback echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope relaxes a host-wide security boundary and is suitable only for an isolated personal-development machine, never a shared or production host. See the canonical Linux ptrace safety note.

Joint with dotnet-assembly-mcp

For decompilation + call graphs:

export ASSEMBLIES_DIR=/path/to/binaries
docker compose -f deploy/docker-compose.yml up -d

Standalone CLI

dotnet-diagnostics-cli is a separate NuGet tool that runs the same Core diagnostics engine as a command you drive yourself — no HTTP server, bearer token, MCP client, or daemon. Useful for scripts, CI, and kubectl exec into the sidecar (the container image ships it on PATH).

If something does not work, run dotnet-diagnostics-cli doctor first — it prints actionable fix: / affects: remediation for common environment and permission problems.

dotnet tool install -g dotnet-diagnostics-cli
dotnet-diagnostics-cli doctor
dotnet-diagnostics-cli processes
dotnet-diagnostics-cli collect --kind counters --pid 1234 --duration 5
dotnet-diagnostics-cli session
# Other one-shot commands
dotnet-diagnostics-cli inspect-heap --pid 1234 --top-types 30 --acknowledge-risk high

# Inside the sidecar container (image bundles the CLI):
kubectl exec -it <pod> -c diagnostics-mcp -- \
  dotnet-diagnostics-cli inspect-heap --pid 1 --acknowledge-risk high

A stateful session REPL keeps collected handles queryable across commands so you can drill in (query --handle <id> --view <view>) without re-collecting, and bind a target pid once with target <pid>:

dotnet-diagnostics-cli session
diag> target 1234
diag(pid 1234)> collect --kind gc --duration 10
diag(pid 1234)> query --handle <id> --view pauseHistogram
diag(pid 1234)> exit

Self-contained per-OS binaries are attached to each Release as dotnet-diagnostics-cli-<version>-<rid>. The downloaded archive is named dotnet-diagnostics-cli-<version>-<rid>; the extracted executable inside it is dotnet-diagnostics-cli (dotnet-diagnostics-cli.exe on Windows). Full reference: docs/cli-reference.md.


BenchmarkDotNet Diagnoser

dotnet-diagnostics-benchmarkdotnet is a separate NuGet package that runs the same Core diagnostics engine as an IDiagnoser, attached in-process to a BenchmarkDotNet child process while it runs — no MCP client, no CLI, no separate host. Useful when a benchmark is slow or allocates more than expected and you want to know why, not just how much.

[DotnetDiagnosticsDiagnoser]   // attach the diagnoser + offenders report (like [MemoryDiagnoser])
public class Workload
{
    [Benchmark]
    [DiagnosticKind(BenchmarkDiagnosticKind.Gc, DurationSeconds = 5)]
    public void AllocateLots() { /* ... */ }
}

Each tagged [Benchmark] gets one EventPipe collection per requested kind (gc, cpu, allocation, contention, threadpool, gcdump, and more) against the child PID; results land in <artifacts>/diagnostics/*.json plus a consolidated *-dotnet-diagnostics-report.md with a per-benchmark "biggest offenders" summary. It is diagnostic, not measurement — pair it with MemoryDiagnoser/ThreadingDiagnoser for clean, publication-grade numbers, and run it on a dedicated diagnostic job. Full reference: src/DotnetDiagnostics.BenchmarkDotNet/README.md.


Tools Overview

17 unified tools. Full schemas and return shapes: docs/tool-reference.md.

The 17 tools at a glance
Tool Purpose
inspect_process Process discovery, capabilities, environment/resources, memory trends, preflight, and evidence-backed triage
collect_events EventCounters/Meters and bounded EventPipe event families (GC, exceptions, activities, logs, JIT, networking, and more)
collect_sample CPU, off-CPU, managed/native allocation, and explicitly gated method-parameter capture
collect_batch Run several collect_sample/collect_events kinds concurrently against one resolved process in one call (eliminates the process-exit race of separate calls)
query_snapshot Re-project retained handles into call trees, diffs, histograms, events, roots, and other focused views
inspect_heap Live or dump heap walk with retained-type, root, retention-path, and async-state-machine drilldowns
get_bytes Materialize authorized module, PDB, dump, or trace bytes from a server-side artifact
discover_azure Configuration-gated App Service, Container Apps, and AKS discovery
collect_process_dump Write a Mini / Triage / WithHeap / Full dump to disk
collect_thread_snapshot Managed thread states, stacks, SyncBlock lock graph, and deadlock evidence
capture_method_bytes Read JIT-emitted native bytes for a managed method from a live process or dump
start_investigation Build a bounded cold, warm, or hypothesis-driven investigation plan
export_investigation_summary Export portable investigation memory as JSON
compare_to_baseline Compare a current investigation summary with a saved baseline
list_orchestrator Configuration-gated Kubernetes namespace, workload, pod, and investigation inventory
attach_to_pod Configuration-gated sidecar/ephemeral-container attach and investigation-handle creation
detach_from_pod Close an orchestrated investigation and release its transport resources

Documentation

📖 docs/ is the documentation hub — start with docs/client-setup.md for the stdio-vs-HTTP choice, then use the rest of the hub for the tool reference, CLI reference, the BenchmarkDotNet diagnoser, investigation playbooks, output examples, authorization/scopes, and deployment guides (Kubernetes, Helm, Azure, AWS, GCP).

Before any production rollout, complete the production-readiness go/no-go checklist.


Goals

  • No prior instrumentation for standard diagnostics — EventPipe and ClrMD work through diagnostic IPC without target code changes
  • Explicit sensitive attach boundary — method-parameter capture is opt-in dynamic profiler instrumentation, not a passive collector
  • Cross-platform — Linux + Windows, containers first-class
  • Graceful NativeAOT — unsupported tools return not_supported, not crashes
  • LLM-friendly — summarized JSON, not raw .nettrace

Build & Test

dotnet build
dotnet test

Requires .NET 10 SDK (pinned in global.json).

Contributor setup (shared dev instance)
scripts/local-mcp.sh start     # builds + starts in background
scripts/local-mcp.sh status
scripts/local-mcp.sh logs -f
scripts/local-mcp.sh stop

Add to ~/.copilot/mcp-config.json:

{
  "mcpServers": {
    "dotnet-diagnostics": {
      "type": "http",
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "Authorization": "Bearer demo-local-token-2026" }
    }
  }
}

Roadmap

Phase status
Phase Status Description
1-3 Foundation + Core diagnostics + MCP server
4 GC, exceptions, EventSources, dumps
5 Kubernetes sidecar (deploy/k8s/)
6 Documentation polish
7 Cloud integrations (Azure, AWS, GCP)
8 Tool consolidation into unified discriminator tools
9–15 Diagnostic UX, package surfaces, platform parity, and signal grouping (see CHANGELOG.md)
16 🚧 MCP protocol evolution + external capability gaps — active roadmap #551

License

MIT — see LICENSE.

About

On-demand .NET runtime diagnostics for live CoreCLR apps — no code changes. Ships an MCP server (let an LLM drive the investigation), a standalone CLI, and a BenchmarkDotNet diagnoser, all on one engine: counters, CPU/off-CPU/alloc sampling, heap & thread snapshots, GC/contention/threadpool events, dumps.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages