Why ILens
The default fallback for an AI agent inspecting a compiled .NET assembly is to shell out to ildasm (IL dump) or ilspycmd (full-C# decompile). Both dump an entire file as text, and that output stays in the agent's context as input tokens on every subsequent turn — quickly thousands of tokens spent inspecting a single class. The measurements below are against ICSharpCode.Decompiler.dll 9.1.0.7988 (≈2.4 MB, 741 public types), which is large enough to be representative but small enough to fit in a single decompile.
| Task | ilspycmd cost |
ILens tool | ILens cost |
|---|---|---|---|
| List the public types in a namespace | 1,041,476 tokens | list_types |
1,712 tokens |
| See the API surface of one mid-sized class | 1,041,476 tokens | summarize_type |
895 tokens |
| Find which types expose a given method | 1,041,476 tokens | find_methods |
618 tokens |
Measured against ICSharpCode.Decompiler.dll 9.1.0.7988 on 2026-05-18; token figures are character counts divided by 4, not a real tokenizer.
ILens turns assembly inspection into bounded, targeted lookups instead of full-file dumps — which is what makes it cheap enough to leave registered as an always-available tool.
Overview and scope
ILens is a single self-contained Windows binary that speaks the Model Context Protocol over stdio. It exposes eleven tools across three categories — discovery (find types and methods), inspection (read API surface and run cross-reference analysis), and decompilation (read C# source). Trust posture is narrow on purpose: every tool is read-only, every assembly path is validated against startup-configured allow-roots, no network is opened, and total memory is capped.
ILens is built for inspecting C# assemblies and its output is C#. The metadata-driven tools (list_types, search_types, list_members, find_methods, analyze) read language-agnostic metadata and work on an assembly from any .NET language. The decompiling tools (decompile_type, decompile_method, decompile_property, decompile_event, summarize_type) always emit C# — for an assembly compiled from F#, VB.NET, or C++/CLI they still run, but constructs without a C# equivalent may render unfaithfully.
Built on ILSpy
ILens would be nothing without ILSpy. The decompiler, the analyzer, the type system that lets ILens speak about a .NET assembly in C# terms at all — none of that is ILens's. It belongs to the ILSpy team. The two libraries ILens links — ICSharpCode.Decompiler for the decompiler proper, ICSharpCode.ILSpyX for the analyzer infrastructure — represent more than a decade of careful work; this project is, candidly, a thin MCP shim on top.
That work is MIT-licensed, which is the only reason ILens can exist. If ILens is useful to you, please consider giving ILSpy a star. They earned it.
To be clear: bug reports and feature requests about ILens belong on the ILens issue tracker, not ILSpy's. Please don't take downstream problems to the upstream maintainers — they have enough work of their own.
Installation
System requirements: Windows 10/11 x64. The binary is self-contained — no separate .NET runtime install required.
Recommended: winget
winget install Tadis.ILens
Standard winget portable install: downloads the release ZIP, extracts under %LOCALAPPDATA%\Microsoft\WinGet\Packages\Tadis.ILens_*\, and registers ilens as a command alias in %LOCALAPPDATA%\Microsoft\WinGet\Links\ (already on your user PATH from any prior winget portable use). No admin elevation needed.
For alternative installers see Appendix A; for updating and uninstalling see Appendix B; for installation troubleshooting see Appendix C.
Integrating with Claude Code
Add an entry to .mcp.json in your project root. The command field is the literal string "ilens" — the installer adds the binary to your user PATH, so no full path is needed:
{
"mcpServers": {
"ilens": {
"command": "ilens",
"args": [
"--allow-root", "C:\\path\\to\\dlls"
]
}
}
}
A first install needs a full Claude app restart, not just a new session. Two independent gates have to clear before a freshly installed ilens actually launches.
Gate 1 — PATH propagation. Windows captures a process's PATH from the registry at process start, and later registry changes do not retroactively reach already-running processes. If ilens is a brand-new entry on user PATH — true for a first-ever winget portable install (which adds %LOCALAPPDATA%\Microsoft\WinGet\Links\) and for any install.ps1 run (which adds %LOCALAPPDATA%\Programs\ILens\) — an already-running Claude app holds a stale environment, and every Claude Code session inside it inherits that stale environment from the parent app. Subsequent winget updates of an already-installed package don't touch PATH and don't trigger this gate.
Gate 2 — MCP-server registration. Claude Code reads .mcp.json only at session start; editing .mcp.json inside a running session does not register the new server in that session.
Recovering takes both: (a) fully exit and relaunch Claude app so the new app process gets the current registry PATH, and (b) start a new Claude Code session in the relaunched app. Existing Claude Code sessions survive a Claude app restart — the relaunched app reattaches to surviving session processes with their original environments intact — so a resumed session won't pick up either change. Only a freshly-spawned session inside the freshly-launched app clears both gates.
Recipe for a first install: close Claude app entirely → install via winget (or install.ps1) → relaunch Claude app → start a new Claude Code session (not a resumed one).
Each --allow-root flag adds a directory tree from which assemblies may be loaded. Without any --allow-root flags, the server cannot load any assemblies. Tool calls supply the assembly path per request; the server validates that the path is inside one of the configured roots, rejects path traversal (..), resolves symbolic links and junctions before re-checking containment, and bounds total memory with an aggregate budget (default 200 MB, --max-total-size <MB>).
Tool calls then specify which assembly to inspect:
list_types(assembly="C:\\path\\to\\dlls\\MyApp.dll", namespaceName="MyApp.Models")
The list_allowed_roots tool surfaces the configured roots so the agent can discover what's reachable.
For the equivalent claude mcp add CLI command see Appendix D; for Claude Desktop see Appendix E.
Project-level guidance
Registering the server makes the tools available. A short hint in the consuming project's CLAUDE.md is what makes Claude actively prefer them over running dotnet / ildasm via Bash, reading assembly files directly, or web-searching for source. Drop the following block into your project's CLAUDE.md and replace <allow-root> with the directory configured above:
## Inspecting .NET assemblies
Assemblies under `<allow-root>` are reachable through the `ilens` MCP server.
Prefer ILens tools over running `ildasm` / `ilspycmd` via Bash, reading assembly
files directly, or web-searching for source.
- Discovery: `search_types` (substring match), `list_types` (whole namespace),
`find_methods` (signature search).
- Reading: `summarize_type` (public surface, no bodies), `list_members`
(filtered surface), `decompile_type` (full C#), `decompile_method` (single
method body), `decompile_property` / `decompile_event` (full property or
event declaration with accessor bodies, by unprefixed name).
- Cross-references: `analyze` with `kind` set to one of `UsedBy`,
`InstantiatedBy`, `ExposedBy`, `ExtensionMethods`, `AppliedTo`,
`OverriddenBy`, `ImplementedBy`, `Uses`, `Implements`, `ReadBy`,
`AssignedBy`. Valid kinds depend on the symbol category.
Per-line walkthrough
- The first paragraph is the load-bearing prefer-MCP rule. Without it, models default to
Bash. - The discovery bullet routes "I don't know the full name" tasks to the right tool: a partial name goes to
search_types, a known namespace goes tolist_types, a method shape goes tofind_methods. - The reading bullet escalates from cheapest to most expensive:
summarize_typefirst,list_memberswhen only part of the surface is needed,decompile_methodfor one method body (ordecompile_property/decompile_eventfor the whole property or event without having to know the IL accessor prefix),decompile_typeonly when full source is required. - The
analyzebullet enumerates thekindenum so the model picks values the schema accepts — using one that does not apply to the symbol category produces an error likeAnalysis kind 'ReadBy' is not valid for Method.
Security model
--allow-rootflags are mandatory: with no roots configured, no assemblies are loadable.- Every request validates the assembly path is inside one of the configured roots — symbolic links and junctions are resolved to their real target first, so a link planted inside a root cannot point the loader outside it.
- Path-traversal segments (
..) are rejected. - Total memory is bounded by an aggregate budget (default 200 MB, set with
--max-total-size <MB>): a load past the budget evicts least-recently-used assemblies, and a single assembly larger than the whole budget is refused. - All MCP tools are read-only — no writes to assemblies, no shell-out, no network calls.
- The server speaks stdio only; it does not open a network port.
- The allow-root bounds which target assemblies can be inspected — it is not the boundary of every file ILens opens: decompilation also reads the target's dependency assemblies (sibling assemblies in its directory, and .NET framework/runtime assemblies) as type-resolution context.
Tool reference
Every tool takes the assembly to inspect as a path that must resolve inside one of the configured allow-roots. Every tool is read-only and returns a single string.
Discovery
find_methodsread-only
Search the assembly for methods matching a signature. Combine any of: name pattern, return type, parameter types, parameter count, declaring namespace, declaring-type pattern, accessibility. Type patterns match by short or full name (generics erased at the top level, Nullable<T> unwrapped, arrays as T[]). Constructors and property/event accessors are excluded; operator methods (op_*) are included.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- namePattern optional
string - Case-insensitive substring filter on method name.
- returns optional
string - Return-type pattern (e.g.
bool,IEnumerable,String). - parameterTypes optional
string[] - Ordered list of parameter-type patterns. The method must have exactly this many parameters, each matching the pattern at its position.
- parameterCount optional
int? - Exact parameter count. If
parameterTypesis also set, the two must agree. - declaringNamespace optional
string - Exact declaring namespace, e.g.
System.IO. - declaringTypePattern optional
string - Case-insensitive substring filter on declaring type's short name.
- accessibility optional
AccessibilityFilter - Default
PublicProtected. - limit optional
int? - Cap on result lines. Default 50.
Returns: one line per matching method (declaring-type.name(parameter-list) → return-type), sorted by declaring type, then method name. The first line is a count; a truncation line is appended if more matches exist beyond the limit.
Errors:
- Path validation failure (any of the Troubleshooting rows below).
parameterCountcontradictsparameterTypes.Length.
See also: analyze for assignability-aware exploration; list_members when you already know the declaring type.
list_allowed_rootsread-only
List the directories from which assemblies can be loaded. Any assembly parameter passed to other tools must resolve inside one of these.
Parameters: none.
Returns: one root per line, or "No allowed roots configured. The server cannot load any assemblies." when the server was launched without --allow-root.
Errors: none.
See also: every other tool — this is the discovery entry point for what's reachable.
list_typesread-only
List all types in a namespace. Returns fully qualified type names so they can be fed into other tools verbatim.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- namespaceName required
string - Namespace to list types from, e.g.
System.IO.
Returns: a count followed by one fully qualified type name per line, alphabetized by short name. Returns "No types in namespace '{namespaceName}'" when the namespace is empty or unknown.
Errors: path validation failure.
See also: search_types when you don't know the namespace.
search_typesread-only
Search for types by name pattern. Matching is a case-insensitive substring against each type's short name (the namespace is not part of the match). Results are returned as fully qualified names so they can be fed into other tools verbatim. Returns up to 50 matches.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- pattern required
string - Pattern matched as a case-insensitive substring against each type's short name. E.g.
StreamfindsMemoryStream,FileStream,BufferedStream.
Returns: a count followed by one fully qualified type name per line, alphabetized. A truncation note is appended if there are more than 50 matches.
Errors: path validation failure.
See also: list_types when you already know the namespace.
Inspection
analyzeread-only
Run cross-reference analysis on a type or member. Omit memberName to analyze the type itself. Valid kinds depend on the symbol category:
| Category | Valid kinds |
|---|---|
| Type | UsedBy, InstantiatedBy, ExposedBy, ExtensionMethods, AppliedTo, ImplementedBy |
| Method | UsedBy, OverriddenBy, ImplementedBy, Uses, Implements |
| Property | OverriddenBy, ImplementedBy |
| Field | ReadBy, AssignedBy |
| Event | OverriddenBy, ImplementedBy |
AppliedTo requires an Attribute-derived type — it errors on a non-attribute type. ImplementedBy on a type requires an interface — it errors on a non-interface type.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name.
- kind required
AnalysisKind - Analysis kind. See table above.
- memberName optional
string - Member name. Omit (or empty) to analyze the type itself.
- parameterCount optional
int? - Method overload disambiguator (only relevant when
memberNameresolves to a method). - parameterTypes optional
string[] - Ordered parameter-type patterns for same-arity overload disambiguation. Same loose matching as
find_methods. - limit optional
int? - Cap on result lines. Default 50.
Returns: a header line (analysis name + analyzed symbol + provenance for inherited / extension-method origins), followed by one reference per line. Truncates at limit with a summary tail.
Errors:
- Path validation failure.
- Type or member not found.
- Overload not disambiguated (the message lists all candidates).
parameterCountcontradictsparameterTypes.Length.kindnot valid for the resolved symbol's category (the message lists valid kinds).kind: AppliedToon a type that does not derive fromSystem.Attribute.kind: ImplementedByon a type that is not an interface.
See also: find_methods for signature-based search; decompile_method to read a method body found via UsedBy.
list_membersread-only
List members of a type, grouped by kind (Methods, Properties, Fields, Events). Returns one signature per line — no method bodies, no XML doc. Filter by member kind, accessibility, and case-insensitive name pattern. Cheaper than summarize_type when you only need part of the API surface.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name.
- kinds optional
MemberKind[] - Member kinds to include:
Method,Property,Field,Event. Omit to include all four. - accessibility optional
AccessibilityFilter - Default
PublicProtected. - namePattern optional
string - Case-insensitive substring filter on member name.
- includeInherited optional
bool - Include members declared on base types (stops at
System.Object). Defaultfalse. Overrides hide the base member. - limit optional
int? - Cap on total result lines across all kinds. Default 100.
Returns: the type's full name, then one section per requested kind. Each section line is the member signature, optionally suffixed with the originating type when includeInherited is set. A truncation tail notes how many members were dropped.
Errors: path validation failure; type not found.
See also: summarize_type for the full public surface; find_methods to search across types.
summarize_typeread-only
Summarize the public and protected API surface of a type — signatures only, no method bodies. Use this for quick lookups of available members, fields, and properties.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name.
Returns: the decompiled C# declaration of the type with non-public members stripped, method and accessor bodies replaced with semicolons, and destructors omitted. Interface members are kept as-is (no explicit modifiers).
Errors: path validation failure; type not found.
See also: list_members for a flat, filterable listing; decompile_type for the full source.
Decompilation
decompile_eventread-only
Decompile an event by name and return the full C# event declaration with add / remove accessor bodies. Shorthand for the IL-prefixed path through decompile_method (add_X, remove_X) — use this when you have the event name and want everything about it.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name, e.g.
System.IO.FileSystemWatcher. - eventName required
string - Event name, e.g.
Changed.
Returns: a header comment line (type + event name + inherited-origin tag if applicable) followed by the decompiled C# event declaration.
Errors: path validation failure; type not found; event not found on the type or its base types.
See also: decompile_method for a single accessor body via its IL name.
decompile_methodread-only
Decompile a single method to C# source code. Faster and more focused than decompile_type when you only need one method. Property and event accessors are also reachable by their IL name (get_X, set_X, add_X, remove_X) even though find_methods hides them from generic browsing — for the whole property or event use decompile_property / decompile_event instead.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name.
- methodName required
string - Method name, e.g.
ReadAllText. - parameterCount optional
int? - Number of parameters, to disambiguate overloads.
- parameterTypes optional
string[] - Ordered parameter-type patterns to disambiguate same-arity overloads. Same loose matching as
find_methods.
Returns: a header comment line (type + method name + inherited-origin tag if applicable) followed by the decompiled C# method body.
Errors:
- Path validation failure.
- Type not found.
- Method not found on the type, its base types, or as an extension method.
- Multiple overloads match and neither
parameterCountnorparameterTypesnarrows to exactly one (the message lists all candidates). parameterCountcontradictsparameterTypes.Length.
See also: decompile_property / decompile_event for the whole property or event by unprefixed name.
decompile_propertyread-only
Decompile a property by name and return the full C# property declaration with its accessor bodies. Shorthand for the IL-prefixed path through decompile_method (get_X, set_X) — use this when you have the property name and want everything about it. For indexer overloads, use decompile_method on get_Item / set_Item with parameterTypes; this tool rejects ambiguous indexer names with the candidates listed.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name, e.g.
System.IO.FileInfo. - propertyName required
string - Property name, e.g.
Length.
Returns: a header comment line (type + property name + inherited-origin tag if applicable) followed by the decompiled C# property declaration.
Errors: path validation failure; type not found; property not found on the type or its base types; ambiguous property name (indexer overload — the message lists candidates and points at decompile_method).
See also: decompile_method for a single accessor body.
decompile_typeread-only
Decompile a type to full C# source code. Use this to understand implementation details, method bodies, and internal logic.
Parameters
- assembly required
string - Path to the .NET assembly to inspect (must be under an allowed root).
- typeName required
string - Fully qualified type name, e.g.
System.String.
Returns: the full decompiled C# source of the type.
Errors: path validation failure; type not found.
See also: summarize_type for the signature-only view; decompile_method for a single body.
Troubleshooting
Tool calls throw on every recoverable failure; the MCP transport wraps the message as "Error: ArgumentException: <message>". Common messages mapped to their cause:
No allowed roots configured. The server cannot load any assemblies.
The server was launched without any --allow-root flags. Add one to the args array in .mcp.json and restart the Claude Code session.
Path is not under any allowed root: ...
The assembly parameter points outside every configured root. The error lists every root for comparison. Either pass a path inside an existing root, or add a new --allow-root flag and restart the session.
Path contains '..' which is not allowed: ...
Defense-in-depth rejection of raw .. segments. Pass an absolute path (or a path without parent-directory traversal).
Path resolves (via a symbolic link or junction) to a location outside any allowed root: ...
A symbolic link or junction inside an allow-root pointed outside every root. Move the assembly into a real subdirectory of an allow-root, or add the link's target as an additional allow-root.
Assembly file does not exist: ...
The path is well-formed and inside a root, but the file is missing. Check the spelling and that the assembly has been built.
Assembly is N MB, which exceeds the M MB total memory budget on its own: ...
A single assembly larger than the whole budget can never fit, even with an empty cache. Raise the budget with --max-total-size <MB> in .mcp.json's args array and restart the session. The default budget is 200 MB.
Type not found: ...
Returned by every type-resolving tool when the fully qualified name does not match any type in the assembly. Use search_types with a substring of the short name to find the correct full name.
Method '...' not found on ..., its base types, or as an extension method.
The method name is not declared on the type, on any base type up to System.Object, or as an extension method on the assembly's namespaces. Use list_members with includeInherited: true or find_methods with a name pattern to find the correct name.
'X' on Y has N matching overloads; pass parameterCount or parameterTypes ...
The method name resolved to multiple overloads and neither hint narrowed it to one. The error lists every candidate with its full signature. Re-call with parameterCount, or with parameterTypes for same-arity disambiguation.
parameterCount (N) contradicts parameterTypes.Length (M).
You passed both disambiguators with inconsistent values. Drop one or align the two.
Member '...' on ... is ambiguous — it exists as more than one member kind (...).
analyze with a memberName that resolves to multiple kinds (e.g. a method and a property with the same name on the same type). Use the kind-specific decompile tool (decompile_property, decompile_event) or pass parameterTypes to force the method resolution path.
Analysis kind '...' is not valid for ....
You passed an AnalysisKind that does not apply to the resolved symbol's category. The error lists the valid kinds for that category — see the analyze reference above.
Analysis kind 'AppliedTo' requires an attribute type ...
The AppliedTo kind only makes sense for a type that derives from System.Attribute. Use a different kind, or analyze an attribute type instead.
Analysis kind 'ImplementedBy' on a type requires an interface ...
The type-level ImplementedBy kind only makes sense for an interface. Use a different kind, or analyze an interface type instead.
Appendix A: Alternative installers
For machines where winget is unavailable, locked down by policy, or where you'd rather inspect the binary before granting execution. Install procedures only — updating and uninstalling live in Appendix B.
PowerShell one-liner
irm https://raw.githubusercontent.com/tadis174/ILens/main/install.ps1 | iex
Installs to %LOCALAPPDATA%\Programs\ILens\ and adds that directory to your user PATH. No admin elevation needed. Automatically stops any running ILens processes before overwriting the binary, so an in-place upgrade does not fail mid-install.
Manual ZIP
Download ILens-windows-x64.zip from the latest release, extract anywhere, then either add the directory to your PATH or reference the binary by its full path in .mcp.json's command field.
Appendix B: Updating and uninstalling
winget (primary)
winget upgrade Tadis.ILens
winget uninstall Tadis.ILens
PowerShell one-liner
To update, re-run the same one-liner — the script overwrites the install directory in place and stops any running ILens processes first:
irm https://raw.githubusercontent.com/tadis174/ILens/main/install.ps1 | iex
To uninstall:
irm https://raw.githubusercontent.com/tadis174/ILens/main/uninstall.ps1 | iex
Manual ZIP
No automatic update mechanism — re-download ILens-windows-x64.zip and re-extract on each new release. To uninstall, delete the extracted folder (and remove the PATH entry if one was added).
Appendix C: Installation troubleshooting
Antivirus / Windows Defender quarantine of ILens.exe
Self-contained .NET binaries are sometimes false-positive-flagged on first sight. Check quarantine and restore the file, or add an exception for the install directory (%LOCALAPPDATA%\Microsoft\WinGet\Packages\Tadis.ILens_*\ for winget, %LOCALAPPDATA%\Programs\ILens\ for the PowerShell script) before re-running the installer.
SmartScreen "Windows protected your PC" on browser-downloaded ZIP
The winget and PowerShell paths bypass this because they don't carry Mark-of-the-Web. For manual browser downloads, click "More info → Run anyway".
winget upgrade Tadis.ILens failing with remove: Access is denied: "...\ILens.exe"
Any running ILens MCP server (a Claude Code session, Claude Desktop, or another AI client that registered ILens) holds an exclusive file lock on the binary winget is trying to replace. Close every AI client with ILens registered and re-run the upgrade. The PowerShell install.ps1 path handles this automatically by stopping running ILens processes before extraction, so the same upgrade via irm ... | iex does not require manual cleanup — winget's portable installer type has no pre-install hook where an equivalent step could run.
Appendix D: claude mcp add CLI
A programmatic alternative to editing .mcp.json by hand:
claude mcp add ilens ilens -- --allow-root "C:\path\to\dlls"
The "double ilens" is intentional: the first is the MCP server alias (matches the JSON key in .mcp.json), the second is the binary on PATH after install. The -- separator before --allow-root is required so claude mcp add does not consume --allow-root as one of its own options.
Appendix E: Claude Desktop
Claude Desktop's MCP config lives at %APPDATA%\Claude\claude_desktop_config.json. Add an entry equivalent to the Claude Code one:
{
"mcpServers": {
"ilens": {
"command": "ilens",
"args": [
"--allow-root", "C:\\path\\to\\dlls"
]
}
}
}
Claude Desktop has no per-project CLAUDE.md — paste the snippet from Project-level guidance into your Claude Desktop Project's custom instructions instead.
Version, source, license
ILens v6 — released under the MIT License. Source at github.com/tadis174/ILens. Third-party components — including the bundled .NET runtime, ILSpy libraries, and the MCP SDK — are documented in third-party-licenses/INDEX.md alongside the per-source license texts in the release ZIP.