A Blazor/Razor-aware Model Context Protocol server for .NET 10.
BlazeDex indexes your Blazor solution via Roslyn's full compilation
pipeline and resolves every component usage, event-handler binding, and
route back to the exact .razor:line:column through the Razor source
generator's #line directives. No grep. No reflection. No string-match
false positives.
BlazeDex vs other Blazor MCP servers
Reflection-based servers (Components.MCP.Blazor and similar) inspect compiled assemblies — they cannot map usages back to source, cannot detect render-mode conflicts across the render tree, cannot lint dead routes.
Documentation servers (Radzen MCP, MudMCP, Fluent UI Blazor MCP) mirror one vendor library's documentation — they know nothing about your code.
BlazeDex does something neither can: it compiles your own solution through Roslyn's
MSBuildWorkspace, walks the Razor source generator's*.razor.g.csoutput, and exposes 20 tools that answer questions about your components, your routes, your code-behind partials — with every answer anchored to.razor:line:columnthrough the Razor#linemapping.
- You work on a Blazor solution and use Claude Code (or any other MCP-capable AI agent)
- You want semantic answers about component graphs — usages, parameters, render-mode conflicts, dead code — without grepping
- You want a read-only analysis layer that never touches your source tree except through one explicit edit tool
- You want an MCP server that mirrors a specific component library's documentation (→ see Radzen MCP, Fluent UI Blazor MCP, MudMCP)
- You want a reflection-based runtime inspector against a live app (→ see Components.MCP.Blazor)
- You're not using .NET 10 (required — we read the Razor source generator output)
1. Download the latest release and extract:
gh release download v0.1.0 -p "blazedex-mcp-v0.1.0-win-x64.zip"
mkdir -p "/c/tools/blazedex-mcp"
unzip -o blazedex-mcp-v0.1.0-win-x64.zip -d "/c/tools/blazedex-mcp"2. Drop this .mcp.json into your Blazor project's repo root:
{
"mcpServers": {
"blazedex": {
"command": "dotnet",
"args": ["C:\\tools\\blazedex-mcp\\BlazeDex.Mcp.dll"],
"env": {
"BLAZEDEX_SOLUTION": "C:\\path\\to\\YourApp.sln"
}
}
}
}3. Ask Claude Code a question about your Blazor code:
Find every usage of the WidgetExample component.
Claude will call find_component_usages("WidgetExample") and return every
.razor:line:column where the component is instantiated — resolved through
the Razor generator's #line directives. No grep, no false positives from
string matches in comments, no confusion between <MyButton/> and
MyButton.SomeStaticMethod().
Full configuration, environment variables, troubleshooting: §3 onwards.
BlazeDex indexes a Blazor solution via Roslyn's MSBuildWorkspace, walks the
Razor source generator's *.razor.g.cs output, and exposes 20 tools over
stdio JSON-RPC for component discovery, usage tracing, API introspection,
code-quality lints, and a single safe edit.
Concretely: ask Claude find_component_usages("WidgetExample") and you
get back every <WidgetExample …/> markup site in the solution with the
exact .razor:line:column source location, resolved through the Razor
generator's #line directives. No grep. No false positives from string
matches in comments. No extension-method confusion. No chasing the difference
between <MyButton/> and MyButton.SomeStaticMethod().
That single example is the smallest unit of value BlazeDex provides. The other 19 tools follow the same shape — Roslyn-backed, source-anchored, yellow-but-never-red on broken code.
- Version: v0.1.0 (initial public release).
- Runtime: .NET 10 SDK required (
net10.0target framework). - MCP SDK: ModelContextProtocol 1.2.0 (official Anthropic SDK), stdio transport.
- Roslyn: Microsoft.CodeAnalysis.CSharp.Workspaces 5.3.0 +
Workspaces.MSBuild 5.3.0, with
Microsoft.Build.*pinned to 17.11.48 (MSBL001 workaround — see Troubleshooting). - Cache: Tier 1 in-memory always; Tier 2 on-disk SQLite
(
Microsoft.Data.Sqlite10.0.5) for warm restarts. - Tested against: a real Blazor WebAssembly solution (~3 projects, ~500 razor files, cold Roslyn build 10–16 seconds, well within the spec §10 budget of <30 seconds). All 20 tools are exercised end-to-end against this corpus.
- Operating systems: Windows is the primary supported platform and the
only one independently verified in v0.1.0. macOS and Linux should work via
the portable MSBuild runtime that
MSBuildLocatorresolves at startup, but they are not independently verified in v0.1 — please file an issue if you exercise BlazeDex on a non-Windows host so we can capture the gotchas. - Test target architecture: Blazor WebAssembly with the standard
{Component}.razor+{Component}.razor.cspartial-class convention. Blazor Server and Blazor United (Interactive Server / WebAssembly / Auto render modes) parse correctly via the same generator pipeline; the render-mode lints in particular target the Blazor United model. - Thread model: single-process MCP server, one solution loaded at a time, fan-out across projects within that solution.
You can either grab a prebuilt zip from the GitHub Releases page or build
from source. Both produce the same binary layout — a self-contained
framework-dependent publish folder containing BlazeDex.Mcp.dll, its
dependencies, and the runtime config files.
Download the latest release zip with the gh CLI and unpack it into a
stable location on disk. We recommend C:\tools\blazedex-mcp\ on Windows
so paths in your .mcp.json stay short and predictable:
gh release download v0.1.0 -p "blazedex-mcp-v0.1.0-win-x64.zip"
mkdir -p "/c/tools/blazedex-mcp"
unzip -o blazedex-mcp-v0.1.0-win-x64.zip -d "/c/tools/blazedex-mcp"After extraction the directory should contain BlazeDex.Mcp.dll plus its
supporting assemblies. Verify with:
ls "/c/tools/blazedex-mcp/BlazeDex.Mcp.dll"Clone the repository and publish in Release configuration. The
dotnet publish step compiles, links, and copies all framework-dependent
assets into a single output folder you can reference from any MCP client.
git clone https://github.com/tomasfil/blazedex.git
cd blazedex
dotnet publish src/BlazeDex.Mcp -c Release -o artifacts/publish/BlazeDex.McpThe artifacts/publish/BlazeDex.Mcp/ folder is now self-sufficient — you
can copy it anywhere, including into C:\tools\blazedex-mcp\ if you want
to keep the same on-disk layout as the prebuilt release.
If you intend to track upstream changes, leave the clone in place and
re-run the dotnet publish command after each git pull. Publishing into
the same output directory will overwrite the previous build cleanly,
provided no MCP client process is currently holding the DLL open.
BlazeDex is configured per-project through a .mcp.json file in your
target Blazor repository (the repo whose solution you want indexed),
plus a small set of environment variables that point the server at the
right .sln or .csproj.
The MCP server itself never reads anything from the project file — it inherits its configuration entirely from the host shell environment that Claude Code (or another MCP client) launches it with.
Copy docs/mcp.json.example from this repository into your target
project's .mcp.json and adjust the paths. A minimal Windows configuration
looks like this:
{
"mcpServers": {
"blazedex": {
"command": "dotnet",
"args": ["C:\\tools\\blazedex-mcp\\BlazeDex.Mcp.dll"],
"env": {
"BLAZEDEX_SOLUTION": "C:\\path\\to\\YourApp.sln"
}
}
}
}The command field invokes the .NET runtime, the args array points it
at the published BlazeDex.Mcp.dll, and the env block injects the
target solution path into the spawned process. The env block is the
most reliable way to scope configuration per project — it travels with
the repo and never depends on user-environment side effects.
The full reference for environment variables lives in docs/env.md.
The four variables you can set are:
BLAZEDEX_SOLUTION— full path to your.slnfile. Preferred. Loads every project in the solution and exposes solution-wide queries (cross-project component usages, dependency-graph drift handling, render-mode lints across the whole graph).BLAZEDEX_PROJECT— full path to a single.csproj. Single-project fallback. Useful when a solution is too large or too broken to load wholesale; you lose cross-project queries but retain everything inside the chosen project.BLAZEDEX_WATCHER— set to0orfalseto disable the passiveFileSystemWatcherfast-path hint. The stat-based fingerprint check still runs on every call, so correctness is unaffected — only the in-process latency hint is disabled.BLAZEDEX_VERIFIED_CLEAN_WINDOW_MS— fast-path window in milliseconds (default250). When the watcher has reported clean status within this window, the next tool call skips the stat-walk. Lower the value if you suspect rapid filesystem changes are slipping through the window.
The MCP server inherits its environment variables from the process that
launches it. If you set BLAZEDEX_SOLUTION only in a single shell,
Claude Code launched from a different shell will not see it.
On Windows, set BLAZEDEX_SOLUTION as a user environment variable
(setx BLAZEDEX_SOLUTION "C:\path\to\YourApp.sln") and restart your
editor, or pass it explicitly through the env block of .mcp.json as
shown above. Passing it through .mcp.json is the most predictable
approach because the configuration travels with the project.
On macOS and Linux, export the variable in ~/.zshrc or ~/.bashrc
(whichever your editor inherits) and restart the editor session.
Per-project .mcp.json env blocks remain the most reliable approach
on every platform.
The first tool call against a fresh process triggers a full Roslyn cold build of the configured solution.
On the test corpus (~3 projects, ~500 razor files) this takes 10–16 seconds and is well inside the spec §10 budget of <30 seconds. Larger solutions scale roughly linearly with project count and file count.
The cold build owns the call — every other tool invocation against the
same process serves from the in-memory Tier 1 cache (sub-millisecond)
until something on disk changes. After the cold build, get_index_status
will report a lastBuildMs value matching the wall-clock time you
observed for the first call.
After Claude Code has launched the MCP server for your project, ask it to call the two diagnostic tools:
ping— should return the literal string"pong". If this fails, the server is not running or stdio transport is misconfigured.get_index_status— should return a JSON object whosestatefield isgreen. The first call may block on the cold build; subsequent calls return immediately.
A typical first run on a green project looks like this:
> ping
"pong"
> get_index_status
{
"state": "green",
"projectCount": 3,
"componentsIndexed": 160,
"lastBuildMs": 11420,
"loadErrors": [],
"parseErrors": []
}
If state comes back as yellow, BlazeDex loaded the solution
successfully but some files have compilation errors — affected rows
will come back with resolved: false and a reason, but unrelated rows
are still fully correct.
If state comes back as red, the whole solution failed to load
(missing SDK, unrestored packages, environment variables not set).
Other tools will return empty results without throwing in the red
state — call get_index_status first to find out why.
- Green — clean build; every row has
resolved: true;loadErrorsandparseErrorsare empty. This is the steady state for a healthy project. - Yellow — best-effort build; some
.razoror.csfiles have errors, the rows that touch them come back withresolved: falseand a human-readable reason in thereasonfield, all other rows are fully correct. Yellow is not a failure — it is BlazeDex's "I gave you what I could" state. Spec §10 calls this the yellow > red discipline: a partial answer beats throwing an exception. - Red — catastrophic failure. The solution did not load at all.
loadErrorslists the diagnostics. Tool calls return empty arrays without throwing, so Claude Code can keep operating against the rest of your toolset while you fix the root cause.
- First call (cold): 10–20 seconds on a ~3-project, ~500-razor-file solution. Roslyn cost dominates; nothing BlazeDex can do other than warm-load from the Tier 2 SQLite cache when available.
- Warm Tier 2 load: 18–22 milliseconds wall-clock from process start to first row. Survives process restart. Same fingerprint check as Tier 1 — the warm-load is never trusted without revalidation.
- Warm Tier 1 call: sub-millisecond. The fingerprint walk is
~50 ms for a 500-file solution, but the watcher fast-path skips it
when the watcher has reported clean within the last
BLAZEDEX_VERIFIED_CLEAN_WINDOW_MS(default 250 ms). - Drift-triggered rebuild: 10–16 seconds, identical to cold. Partial rebuild is deferred — see Known Limitations.
BlazeDex v0.1.0 exposes 20 tools grouped into six functional buckets.
Every tool wraps its body in a try/catch at the MCP boundary;
exceptions become structured error rows in the response and never
propagate to the client.
list_razor_components(glob?, limit, offset)— enumerate every.razorcomponent in the indexed solution, with optional glob filter and pagination. Returns the source path, code-behind path (if present), and the component's fully-qualified name.list_pages(glob?)— list every routable component, derived from@pagedirectives and[Route(…)]attributes on partial classes. Each row carries the route template plus the source location of the directive.find_component(name)— locate a component by short name or fully-qualified name. Returns the canonical row including all codebehind partners (.razor,.razor.cs,.razor.css) when present.find_components_injecting(serviceTypeName)— find every component whose[Inject]chain (including inherited base classes across project boundaries) references a given service type. Useful for blast-radius analysis when changing a DI registration.find_route_definitions(routeTemplateGlob?)— enumerate every route template in the solution, optionally filtered by a glob over the template string. Each row carries the route plus the component that owns it.
find_component_usages(componentName, limit, offset)— return every markup site that instantiates a given component. For example,find_component_usages("WidgetExample")returns every consuming page's.razor:line:columnwithresolved: true. Resolution flows through the Razor generator's#linedirectives viaLocation.GetMappedLineSpan()— never hand-parsed.find_handler_bindings(methodQualifiedName)— find every markup site that wires a code-behind handler throughEventCallback.Factory.Create(this, HandlerName). For example,find_handler_bindings("WidgetExampleBase.HandleValueChanged")returns the.razor:line:columnof the binding site.find_parameter_passers(componentName, parameterName)— find every markup site that passes a value to a specific[Parameter]of a given component. Resolves through the Razor generator'sAddComponentParameter(seq, nameof(Type.PropName), valueExpr)pattern.find_binding_targets(componentName, parameterName)— find every markup site that uses@bind-{Parameter}syntactic sugar against a specific component parameter. Returns both the binding target expression and its.razor:line:columnlocation.find_bind_inventory(componentName?)— solution-wide inventory of@bind-…usage, optionally filtered to one component. Useful for two-way binding audits and migration planning.
get_component_api(componentQualifiedName)— return the full surface area of a component: parameters (with[EditorRequired],[SupplyParameterFromQuery], capture-unmatched-values flags), cascading parameters, render fragments, injects, methods, fields, events, class-level attributes, and generic constraints. Walks the inheritance chain across project boundaries viaINamedTypeSymbol.BaseTypeso base-class members are folded in automatically.get_component_tree(componentName, depth?)— render the component instantiation tree rooted at a given component, walkingOpenComponent<T>invocations recursively up to the optional depth limit. Each node carries the.razor:line:columnsource anchor.get_cascading_chain(componentName)— trace the[CascadingParameter]flow from a consumer back to its provider(s). Limited to the static cascading model in v0.1 — see Known Limitations for what this does not yet cover.
find_unused_components(includeTestProjects?)— list every component in the solution that has zerofind_component_usagesresults. Optionally folds test projects into the analysis. The first stop for dead-code cleanup.find_route_conflicts()— return route templates that resolve ambiguously (two or more components claiming the same@pageliteral, or templates whose constraints overlap). Each conflict row lists every offending component.find_render_mode_conflicts()— return components whose render-mode attributes conflict with their parent's render mode in Blazor United (e.g., aRenderModeServercomponent instantiated inside aRenderModeWebAssemblyparent). Each row carries both anchors.find_dead_routes()— return route definitions that are reachable through@pagedirectives but never linked to from any<NavLink>or programmaticNavigationManager.NavigateTocall. Useful for finding orphaned admin pages and stale endpoints.
add_parameter(componentQualifiedName, parameterName, parameterType, defaultValueExpression?, isEditorRequired?)— the only edit tool in v0.1.0. Adds a[Parameter] public {Type} {Name} { get; set; }declaration to the named component's code-behind partial class via Roslyn'sSymbolEditor. Validates that the parameter does not already exist before writing, runsdotnet formaton the modified file, and surfaces the diff in the response. All other edit operations are deferred to spec Phase 3 (see Known Limitations).
ping()— returns the literal string"pong". Use this as the first call after launching the MCP server to confirm the stdio transport is working.get_index_status()— returns the current state of the index:state(green/yellow/red),projectCount,componentsIndexed,lastBuiltAtUtc,lastBuildMs,loadErrors,parseErrors,tier2Loaded,watcherHealth. The first thing to call when something looks wrong.
The whole solution failed to load. Common causes:
- .NET 10 SDK not installed. BlazeDex targets
net10.0and requires the .NET 10 SDK on the machine running the MCP server. Check withdotnet --list-sdks— you should see at least one10.0.xentry. If you only have older SDKs, install .NET 10 from https://dot.net and restart your MCP host. - NuGet packages not restored. Run
dotnet restoreagainst your target solution at least once before launching the MCP server. The RoslynMSBuildWorkspacecannot resolveProjectReferenceclosures if upstream projects are not restored. - Environment variables not set. Confirm
BLAZEDEX_SOLUTION(orBLAZEDEX_PROJECT) is set in the environment that launches Claude Code. Pass them through theenvblock of.mcp.jsonif your shell environment is unreliable. - Path is wrong or contains the wrong slashes. Use double-backslashes
inside JSON string literals on Windows:
"C:\\path\\to\\YourApp.sln", not"C:\path\to\YourApp.sln". Forward slashes work too:"C:/path/to/YourApp.sln". - Solution file references missing or moved projects. Open the
.slnin a text editor and confirm everyProject("…") = "…", "…"line points at a path that exists relative to the solution file.
If the cause is not obvious from loadErrors, run dotnet build
against the same solution from the command line. Any error there will
reproduce in BlazeDex.
Some files have compile errors. BlazeDex serves what it could resolve;
affected rows come back with resolved: false and a reason string.
This is the spec's yellow > red discipline — yellow is correct behavior, not a bug. Fix the underlying compile errors in your editor and the next tool call will rebuild and clear the yellow state.
If you want to see exactly which files are failing, look at
get_index_status.parseErrors — it lists each affected file with the
diagnostic messages Roslyn raised.
This is the expected Roslyn cold build. The first tool call against a fresh process loads every project in the solution, materializes the Razor generator's syntax trees, and walks them.
Subsequent calls hit the in-memory Tier 1 cache and complete in under a millisecond. On a warm restart the Tier 2 SQLite cache loads in 18–22 milliseconds.
If your cold build exceeds 30 seconds (the spec ceiling), file an
issue with get_index_status output attached so we can tune the
indexer for your project shape.
Look at get_index_status.watcherHealth. If it reports disabled you
have set BLAZEDEX_WATCHER=0 — re-enable it by unsetting the
variable.
If it reports unhealthy the watcher tripped its sliding 10-second /
3-failure recreation budget (typically because the project lives on a
network share or WSL bridge, both of which silently drop
FileSystemWatcher events).
Correctness is unaffected because the per-call stat fingerprint always
runs as the authoritative check; you only lose the latency hint. To
force-disable the watcher and rely entirely on the stat fingerprint,
set BLAZEDEX_WATCHER=0 and restart the MCP host.
If you dotnet build BlazeDex itself while Claude Code is still
holding the MCP process open, you will see file-lock errors on
BlazeDex.Mcp.dll and friends. Quit Claude Code (or the MCP host)
before rebuilding, or build in a different output directory.
The publish output under C:\tools\blazedex-mcp\ is loaded by the
running process and cannot be overwritten in place. The CI release
pipeline already uses dotnet build -c Release and dotnet publish
into a separate artifacts/publish/ folder for exactly this reason.
If you want a self-contained sanity check that does not depend on
Claude Code, run dotnet test --no-build -c Release inside the
BlazeDex repo to exercise the unit-test suite, then launch the server
and call ping followed by get_index_status from your MCP client of
choice. A green state plus a non-zero componentsIndexed confirms
the install is healthy end-to-end.
v0.1.0 is the first publishable cut. The following gaps are
deliberately deferred to later phases — they are tracked in PLAN.md
and the Phase 2/3 specs.
- Edit operations are limited to
add_parameter. Spec Phase 3 (rename, extract component, change-parameter-type, codebehind partial promotion, route refactoring) is deferred. v0.1 deliberately ships one safe edit so the wiring (RoslynSymbolEditor, format pass, response diff, scope lock) is exercised end-to-end without a large surface area to maintain. - Drift handling triggers a full rebuild. When the per-call stat
fingerprint detects any change, BlazeDex rebuilds the entire
solution (10–16 seconds on the test corpus, well inside the
<30-second budget). Partial rebuild via
ProjectDependencyGraphreverse-dep closure is plumbed into the in-memory model but not yet wired into the rebuild path —MSBuildWorkspacedoes not support in-place project reload, so a fresh-workspace-per-affected-project approach cascades to a near-whole-solution reload once transitiveProjectReferenceclosure is required for symbol resolution. Rows are already Roslyn-independent (pre-mapped viaLocation.GetMappedLineSpan()at index-build time) so a future step can switch drift handling to true partial rebuild without schema changes. - CascadingValue tracking is limited to
get_cascading_chain. The static[CascadingParameter]↔<CascadingValue>provider/consumer chain is supported. Dynamic cascading values (programmatic providers, conditional cascade scopes, value-changed re-render flow) are deferred to spec Phase 2. RenderFragmentresolution is partial. The Phase 2 surface splitsRenderFragmentandRenderFragment<T>into a dedicatedfragmentsbucket onget_component_api, but full template-content resolution (which markup actually fills the fragment, with full.razor:line:columnprovenance) is deferred.- Single solution per process. One BlazeDex MCP process handles
one solution. To index two solutions concurrently, register two MCP
servers in your
.mcp.jsonwith distinctBLAZEDEX_SOLUTIONvalues and distinct command invocations. - Non-Windows hosts are not independently verified. macOS and
Linux should work via the portable MSBuild runtime that
MSBuildLocatorresolves at startup, but v0.1.0 ships only with Windows verified end-to-end. - No streaming responses. All tool responses are returned as a
single JSON-RPC message. Very large result sets are paginated via
limit/offsetparameters where the tool supports them.
BlazeDex is source available under the Elastic License 2.0.
In short: you can use, modify, and distribute BlazeDex freely, except:
- You may not provide BlazeDex to third parties as a hosted or managed service where the service gives users access to a substantial set of BlazeDex features or functionality.
- You may not circumvent any license-key functionality.
- You may not alter, remove, or obscure any copyright or licensing notices.
See LICENSE for the full terms. By contributing to this
repository you agree to license your contributions under the same
terms. See CONTRIBUTING.md for the sign-off
requirement.
For the design rationale and the full tool surface specification, see
blazor-razor-mcp.md — the spec document that drove the v0.1 build.
For the execution plan, deferred work, and design decisions made along
the way, see PLAN.md. Both files live in the repository root and are
checked in.
The high-level architecture is:
- stdio transport. All logging is routed to stderr because
stdout is reserved for the JSON-RPC protocol frame.
Console.WriteLineis forbidden in server code. MSBuildLocator.RegisterDefaults()runs as the first line ofProgram.cs, before any MSBuild assembly loads. This is non-negotiable — anything else triggers MSBL001.- Roslyn
MSBuildWorkspaceopens the configured.sln(or fallback.csproj), andGetCompilationAsyncmaterializes the Razor source generator's*.razor.g.cssyntax trees in memory — no files are written to disk. ProjectIndexerwalks the syntax trees once per project, collects rows for every tool, and stashes them in per-projectProjectSnapshotrecords.IndexServiceholds the in-memory Tier 1 cache, runs the per-call stat fingerprint check, and rebuilds from scratch on drift. The fingerprint coverslast-write-time+sizefor every.razor,.razor.cs,.razor.css,.cs,.csproj,_Imports.razor,Directory.Build.props, andglobal.jsonunder each loaded project directory.- Tier 2 SQLite cache (
Microsoft.Data.Sqlite) serializes the row tables on shutdown and warm-loads them on startup in 18–22 milliseconds. The same fingerprint format is used for Tier 1 and Tier 2; warm-load runs the fingerprint check before serving so Tier 2 can never silently serve stale rows. - Passive
FileSystemWatcheris a fast-path hint only. The stat fingerprint always runs as the authoritative check — watcher failures (network shares, WSL bridges, save bursts, symlinks, suspend/resume) cannot cause stale results, only slower paths. - Tool boundary
try/catchwraps every[McpServerTool]method body. Exceptions become structured error rows; nothing escapes to the JSON-RPC client.
The cold-build budget of 10–16 seconds on the test corpus comfortably
meets the spec §10 ceiling of <30 seconds. The anchor case —
find_handler_bindings("WidgetExampleBase.HandleValueChanged")
resolving back to the correct .razor:line:column through the Razor
#line directives — is verified at every gate (cold Roslyn build,
Tier 2 warm-load, and watcher-disabled paths).