This is the single durable technical reference for the repository.
Documentation policy:
README.mdis the user-facing entry point.ARCHITECTURE.mdis the long-lived implementation reference.TODO.mdis backlog only.- Historical plans, migration notes, helper guides, and testing guides should not be recreated unless they add durable information that does not fit here.
Program.cs builds a hosted stdio MCP server and auto-discovers tools from the assembly.
Key startup behavior:
- registers
DecompilerWorkspaceplus the legacy singleton services; - registers
WorkspaceBootstrapService; - configures concise MCP server instructions through
McpServerOptions.ServerInstructions; - calls
.AddMcpServer(...).WithStdioServerTransport().WithToolsFromAssembly(); - initializes
ServiceLocatorwith the built service provider.
Tool shape:
- MCP tools are static methods under
Tools/; - tool methods should return through
ResponseFormatter.TryExecute(...); - tool names exposed to clients are normally snake_case versions of the C# method names.
DecompilerWorkspace is the root of the multi-context model.
Responsibilities:
- loads or replaces one alias at a time;
- tracks the current alias;
- holds the alias-to-session map;
- maps assembly MVID to alias for follow-up routing;
- persists registrations to disk;
- restores registered aliases at startup.
Registry location:
- default path is
LocalApplicationData/DecompilerServer/contexts.json; - if
LocalApplicationDatais unavailable, it falls back to~/.decompilerserver/DecompilerServer/contexts.json.
Each loaded alias owns one DecompilerSession.
A session bundles:
AssemblyContextManagerMemberResolverDecompilerServiceUsageAnalyzerInheritanceAnalyzer
Sessions are isolated per loaded assembly so caches and member resolution stay version-specific.
Owns one loaded assembly context and the expensive decompiler state:
- loaded PE file;
- type system;
- configured
CSharpDecompiler; - indexes and cache-adjacent metadata;
- assembly summary counts and settings.
This is the boundary for one loaded assembly, not the whole process.
MemberResolver owns stable member IDs, normalization, resolution, and resolution caching.
Stable ID format:
<mvid-32hex>:<token-8hex>:<kind-code>
Kind codes:
TtypeMmethod or constructorPpropertyFfieldEeventNnamespace
These IDs are stable for a given assembly MVID and are the basis for cheap follow-up MCP calls.
DecompilerService handles source retrieval and source caching.
Important behaviors:
- caches decompiled documents and related source payloads;
- supports line-range retrieval;
- supports focused entity decompilation through
DecompileEntitySnippet(...). - decompiled non-type members are cached and returned as member-scoped snippets rather than whole containing types.
DecompileEntitySnippet(...) is the correct path for compare workflows that need one concrete member body rather than the whole containing type.
These services provide graph-style analysis:
- usages;
- callers and callees;
- string literal search;
- base and derived types;
- implementations;
- overrides and overloads.
find_callees is callee-shaped, not usage-shaped. Items should identify the target through targetMemberId when the operand resolves to a local assembly member, and through symbol, declaringType, opcode, offset, operandTokenHex, and resolution for local, external, or unresolved metadata operands. Legacy inMember/inType aliases may remain for compatibility, but new clients should prefer the target-specific fields.
TypeSurfaceComparer defines the shared semantics for structural type diffs.
It is the authority for:
- direct-member enumeration;
- member-kind normalization;
- compiler-generated type filtering;
- type-surface change detection used by both
compare_symbolsandcompare_contexts.
ResponseFormatter centralizes tool response formatting and exception wrapping.
Current response conventions:
- camelCase JSON;
- null values omitted where appropriate;
- structured errors instead of throwing across the MCP boundary.
Structured error responses preserve top-level status, message, and details fields, and also include error.code, error.message, error.details, and optional error.hints. Symbol-resolution failures should use stable codes such as type_not_found, member_not_found, ambiguous_member, wrong_symbol_kind, invalid_member_id, and no_assembly_loaded.
The repository currently supports both the workspace model and the older single-context fallback. New work should target the workspace-aware path.
ToolSessionRouter is the only routing layer tools should use.
Rules:
- discovery or search tools with no
memberIduseGetForContext(contextAlias); - follow-up tools that take
memberIduseGetForMember(memberId, contextAlias); - explicit
contextAliason a member-based tool wins over MVID routing; - without an explicit alias, member-based tools route by the
memberIdMVID and then fall back to the current alias.
ServiceLocator bridges static MCP tool methods to DI-managed services.
Important behavior:
- production uses the global provider;
- tests can override the provider thread-locally;
- current-session services are resolved from the workspace when available.
The intended workflow is to keep multiple assemblies loaded and address them by alias.
Operational rules:
load_assemblyloads or replaces one alias;- omitted aliases normalize to the default alias
default; makeCurrentcontrols whether the loaded alias becomes the current one;list_contextsreports loaded aliases and which one is current;select_contextchanges the default alias;unloadcan unload one alias or all aliases, and removes persisted registrations by default;unload(..., preserveRegistration: true)keeps restart-restore registrations while unloading memory;statusreports current alias plus loaded contexts when the workspace is active;get_server_stats(contextAlias)reports detailed cache, index, and performance diagnostics for one alias or the current alias.
Startup behavior:
WorkspaceBootstrapServicerestores persisted registrations;- startup logs announce each restored alias and its resolved assembly path;
- failed restores are logged as warnings but do not abort server startup.
These are the contracts other work should preserve unless there is a deliberate breaking change.
Tool output should stay structured JSON. Do not move compare or overview tools toward pre-rendered text output when structured data is feasible.
Search-style and overview-style endpoints should use:
limitcursoritemsnextCursortotalEstimatewhen applicable
compare_contexts uses integer-offset cursors today.
get_il supports the same limit/cursor paging pattern for instruction lists and also accepts startOffset/endOffset windows when callers need a byte-offset slice around a suspected anchor.
Once a discovery tool returns a memberId, the caller should be able to use follow-up tools without resupplying the alias. That behavior depends on the MVID prefix and must remain reliable.
Unknown-assembly exploration should stay inside MCP tools:
- use
search_symbolswhen the caller has a fragment or is unsure whether a name is a type or member; - use
resolve_member_idfirst for fully-qualified or XML-doc-like guessed symbols such asNamespace.Type.MemberorM:Namespace.Type.Member; - use
search_typesfor type-only discovery andsearch_membersfor member-only discovery; - use
list_membersorget_members_of_typeafter a type is found; - if a member-based tool receives a stale or human-entered symbol, return structured candidates and suggested next tool calls rather than only
Invalid member ID. - if
search_symbolsreceivesType.MissingMemberand the type resolves, return a diagnostic plus the type and direct members instead of an empty success.
Program.ServerInstructions is intentionally short and workflow-oriented.
It should:
- steer clients toward
search_symbols,resolve_member_id,list_members, structured errors, and common parameter names; - complement the tool schemas rather than duplicate the full README or tool reference;
- stay concise enough to be useful in the MCP handshake.
If the workflow changes, update Program.ServerInstructions, the Codex skill, and this section together.
Comparison is intentionally layered so the caller can stay cheap on context and only drill in when needed.
compare_contexts is the alias-level structural overview.
Semantics:
- compares type presence and direct member surface between two aliases;
- returns structured summary counts plus type items;
- filters by
namespaceFilterand optionaldeeptraversal; - filters compiler-generated types by default;
- includes unchanged types only when
includeUnchangedis true.
Status meanings:
added: type only exists on the right alias;removed: type only exists on the left alias;changed: type exists in both aliases and its direct member surface changed;unchanged: type exists in both aliases and its direct member surface is the same.
changed does not mean arbitrary method bodies changed. It means the direct member surface changed according to TypeSurfaceComparer.
compare_symbols is the drill-down tool.
Supported modes:
symbolKind: "type"withcompareMode: "surface"symbolKind: "method"withcompareMode: "surface"orcompareMode: "body"symbolKind: "field" | "property" | "event"withcompareMode: "surface"
Accepted member symbol formats:
Namespace.Type:MemberNameNamespace.Type.MemberName
Surface semantics:
- type compare reports added, removed, and changed direct members;
- member compare reports left and right signatures plus
signatureChanged.
Body semantics:
- method-only;
- uses
DecompilerService.DecompileEntitySnippet(...); - returns
bodyChanged,bodyDiff, and compactdiffStats.
Compatibility note:
compareMode: "source"is accepted as an alias for methodcompareMode: "body".
This limitation is intentional. Non-method symbols do not have one coherent cross-kind meaning for "body".
These are current boundaries, not bugs.
compare_contextsis structural and does not inspect method bodies.compare_symbols(compareMode: "body")is method-only.get_ilcurrently supports"IL"output, notILAst.get_ilreturns real IL instructions when the method has a body; abstract, extern, and interface methods reportno_il_body.- rename detection across aliases is not special-cased; a rename appears as remove-plus-add at the type or member level.
- compiler-generated noise is excluded from context-wide compare by default.
Tests use xUnit and real compiled test assemblies.
Important fixtures:
Tests/ServiceTestBase.csTestLibrary/EmbeddedSourceTestLibrary/NestedNoSymbolsTestLibrary/Tests/TemporaryAssemblyBuilder.cs
Coverage focus:
- service behavior on real assemblies;
- workspace lifecycle and persistence;
- context-aware routing;
- structured tool output;
- compare behavior under controlled version drift.
Test naming pattern:
- use dedicated
*ToolTests.csfiles for MCP tool behavior; - use service-level test files when the behavior is below the MCP boundary.
When adding or changing tools:
- keep tool methods static under
Tools/; - route through
ToolSessionRouter, not ad hoc service resolution; - use
ResponseFormatter.TryExecute(...); - prefer shared helpers such as
TypeSurfaceComparerover re-implementing comparison semantics; - prefer structured JSON over preformatted diff text for overview endpoints.
When changing compare behavior:
- keep
compare_contextsand type-levelcompare_symbolsaligned throughTypeSurfaceComparer; - keep method body diff opt-in;
- do not broaden
"body"semantics without a clear symbol-kind-specific design.
When changing documentation:
- update
README.mdfor user-facing workflow changes; - update
ARCHITECTURE.mdfor durable implementation or contract changes; - update
TODO.mdonly for backlog changes.
After code changes, run:
dotnet format DecompilerServer.sln
dotnet test -c Release --no-restoreAfter documentation cleanup or API reshaping, also run a reference sweep:
rg -n "HELPER_METHODS_GUIDE|TESTING\\.md|MULTI_VERSION_WORKSPACE_PLAN|CommonImplementorGuide|ARCHITECTURE\\.md" .