Skip to content

Commit f1dfd94

Browse files
committed
Prepare DecompilerServer 1.3.5
1 parent 3263f8e commit f1dfd94

40 files changed

Lines changed: 1584 additions & 361 deletions

.github/copilot-instructions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ Read these files first:
1414
- Reuse `TypeSurfaceComparer` for type-surface semantics instead of duplicating compare logic.
1515
- Use `DecompilerService.DecompileEntitySnippet(...)` for focused compare body retrieval.
1616
- Prefer structured JSON output over pre-rendered diff text for overview commands.
17-
- For unknown foreign code, start with `search_symbols`, then `list_members`/`get_members_of_type`, then source or caller/callee tools. Do not fall back to shell scans after one bad member guess unless MCP diagnostics are exhausted.
17+
- For unknown foreign code, start with `search_symbols` for fragments. For fully-qualified or XML-doc-like guessed symbols, start with `resolve_member_id` so structured `member_not_found` diagnostics can resolve the type and suggest candidates.
18+
- After a type resolves, use `list_members`/`get_members_of_type`, then source or caller/callee tools. Do not fall back to shell scans after one bad member guess unless MCP diagnostics are exhausted.
1819

1920
## Workspace and Compare Expectations
2021

ARCHITECTURE.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ These services provide graph-style analysis:
105105
- implementations;
106106
- overrides and overloads.
107107

108+
`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.
109+
108110
### TypeSurfaceComparer
109111

110112
`TypeSurfaceComparer` defines the shared semantics for structural type diffs.
@@ -161,7 +163,8 @@ Operational rules:
161163
- `select_context` changes the default alias;
162164
- `unload` can unload one alias or all aliases, and removes persisted registrations by default;
163165
- `unload(..., preserveRegistration: true)` keeps restart-restore registrations while unloading memory;
164-
- `status` reports current alias plus loaded contexts when the workspace is active.
166+
- `status` reports current alias plus loaded contexts when the workspace is active;
167+
- `get_server_stats(contextAlias)` reports detailed cache, index, and performance diagnostics for one alias or the current alias.
165168

166169
Startup behavior:
167170
- `WorkspaceBootstrapService` restores persisted registrations;
@@ -187,24 +190,28 @@ Search-style and overview-style endpoints should use:
187190

188191
`compare_contexts` uses integer-offset cursors today.
189192

193+
`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.
194+
190195
### Member-ID Follow-Up Flow
191196

192197
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.
193198

194199
### Symbol Exploration Flow
195200

196201
Unknown-assembly exploration should stay inside MCP tools:
197-
- use `search_symbols` when the caller has a partial, qualified, or guessed name;
202+
- use `search_symbols` when the caller has a fragment or is unsure whether a name is a type or member;
203+
- use `resolve_member_id` first for fully-qualified or XML-doc-like guessed symbols such as `Namespace.Type.Member` or `M:Namespace.Type.Member`;
198204
- use `search_types` for type-only discovery and `search_members` for member-only discovery;
199205
- use `list_members` or `get_members_of_type` after a type is found;
200206
- 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`.
207+
- if `search_symbols` receives `Type.MissingMember` and the type resolves, return a diagnostic plus the type and direct members instead of an empty success.
201208

202209
### MCP Server Instructions
203210

204211
`Program.ServerInstructions` is intentionally short and workflow-oriented.
205212

206213
It should:
207-
- steer clients toward `search_symbols`, `list_members`, structured errors, and common parameter names;
214+
- steer clients toward `search_symbols`, `resolve_member_id`, `list_members`, structured errors, and common parameter names;
208215
- complement the tool schemas rather than duplicate the full README or tool reference;
209216
- stay concise enough to be useful in the MCP handshake.
210217

DecompilerServer.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<TargetFramework>net10.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8-
<Version>1.3.4</Version>
8+
<Version>1.3.5</Version>
99
</PropertyGroup>
1010

1111
<ItemGroup>

Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ namespace DecompilerServer;
99
public partial class Program
1010
{
1111
internal const string ServerInstructions = """
12-
DecompilerServer inspects loaded .NET assemblies. Use search_symbols first when you have a partial, qualified, or guessed name.
12+
DecompilerServer inspects loaded .NET assemblies. Use search_symbols first for fragments; use resolve_member_id first for fully-qualified or XML-doc-like guessed symbols.
1313
Common parameter names: search_types/search_members use query, not pattern; resolve_member_id/get_decompiled_source/find_usages use memberId; find_callers/find_callees/get_overrides use methodId; get_types_in_namespace uses ns.
1414
After resolving a type, use list_members or get_members_of_type before guessing method names. If a lookup fails, inspect structured error.details, candidates, and hints before retrying.
15+
Use status/list_contexts to confirm loaded aliases; use get_server_stats for detailed cache/index diagnostics.
1516
""";
1617

1718
internal static void ConfigureMcpServerOptions(McpServerOptions options)

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ load_assembly({
237237
```text
238238
list_contexts({})
239239
status({})
240+
get_server_stats({ "contextAlias": "rw16" })
240241
```
241242

242243
**Search and decompile:**
@@ -269,12 +270,13 @@ Once you have a `memberId`, follow-up tools normally route to the correct loaded
269270
For foreign-code exploration, prefer this path before falling back to shell tools:
270271

271272
1. Load the assembly with `load_assembly` and verify the active alias with `list_contexts` or `status`.
272-
2. Start broad with `search_symbols` when you have a partial or guessed name; use `search_types` only when you specifically want types.
273+
2. Start broad with `search_symbols` when you have a fragment; use `resolve_member_id` first when you have a fully-qualified or XML-doc-like guess such as `Namespace.Type.Member` or `M:Namespace.Type.Member`.
273274
3. Use `list_members` or `get_members_of_type` on the resolved type before guessing method IDs.
274275
4. Fetch code with `get_decompiled_source`, `get_source_slice`, or `plan_chunking`.
275-
5. Move outward with `find_callers`, `find_usages`, `find_callees`, `get_il`, or compare tools.
276+
5. Move outward with `find_callers`, `find_usages`, `find_callees`, `get_il`, or compare tools. `find_callees` returns callee-focused `targetMemberId`, `symbol`, `opcode`, `offset`, and `resolution` fields; external calls are explicit instead of pretending to be canonical local IDs.
277+
6. Use `get_server_stats` when you need cache/index/performance diagnostics for one alias; `status` is the cheaper current-alias overview.
276278

277-
If a guessed member does not exist, member-based tools return structured error hints with likely candidates and concrete next tool calls. For example, a stale method guess can still resolve the type and point you at nearby overrides instead of forcing a `grep` or `monodis` fallback.
279+
If a guessed member does not exist, member-based tools return structured error hints with likely candidates and concrete next tool calls. `search_symbols` also degrades gracefully for `Type.MissingMember`: if the type resolves but the member does not, it returns the type, its direct members, and a diagnostic instead of an empty success.
278280

279281
**Compare aliases:**
280282

@@ -319,11 +321,11 @@ The repo includes a portable skill at [skills/decompiler-mcp/SKILL.md](skills/de
319321

320322
The GitHub `Release` workflow is triggered by pushing a tag that starts with `v` and matches the project version in `DecompilerServer.csproj`.
321323

322-
For example, if the project version is `1.3.4`:
324+
For example, if the project version is `1.3.5`:
323325

324326
```bash
325-
git tag -a v1.3.4 -m "Release v1.3.4"
326-
git push origin v1.3.4
327+
git tag -a v1.3.5 -m "Release v1.3.5"
328+
git push origin v1.3.5
327329
```
328330

329331
### Documentation

Services/AssemblyContextManager.cs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class AssemblyContextManager : IDisposable
2121
private PEFile? _peFile;
2222
private ICompilation? _compilation;
2323
private CSharpDecompiler? _decompiler;
24+
private DecompilerSettings? _settings;
2425
private UniversalAssemblyResolver? _resolver;
2526
private bool _disposed;
2627
private long _contextVersion;
@@ -99,12 +100,8 @@ public void LoadAssemblyDirect(string assemblyPath, string[]? additionalSearchDi
99100
_compilation = new DecompilerTypeSystem(_peFile, _resolver);
100101

101102
// Create decompiler with enhanced settings
102-
var settings = new DecompilerSettings
103-
{
104-
UsingDeclarations = true,
105-
ShowXmlDocumentation = true,
106-
NamedArguments = true
107-
};
103+
var settings = CreateDefaultDecompilerSettings();
104+
_settings = CloneSettings(settings);
108105
_decompiler = new CSharpDecompiler(_peFile, _resolver, settings);
109106

110107
// Extract MVID
@@ -162,12 +159,8 @@ public void LoadAssembly(string gameDir, string assemblyFile = "Assembly-CSharp.
162159
_compilation = new DecompilerTypeSystem(_peFile, _resolver);
163160

164161
// Create decompiler with enhanced settings
165-
var settings = new DecompilerSettings
166-
{
167-
UsingDeclarations = true,
168-
ShowXmlDocumentation = true,
169-
NamedArguments = true
170-
};
162+
var settings = CreateDefaultDecompilerSettings();
163+
_settings = CloneSettings(settings);
171164
_decompiler = new CSharpDecompiler(_peFile, _resolver, settings);
172165

173166
// Extract MVID
@@ -200,6 +193,23 @@ public CSharpDecompiler GetDecompiler()
200193
}
201194
}
202195

196+
/// <summary>
197+
/// Get a snapshot of the current decompiler settings.
198+
/// </summary>
199+
public DecompilerSettings GetSettings()
200+
{
201+
_lock.EnterReadLock();
202+
try
203+
{
204+
EnsureLoaded();
205+
return CloneSettings(_settings!);
206+
}
207+
finally
208+
{
209+
_lock.ExitReadLock();
210+
}
211+
}
212+
203213
/// <summary>
204214
/// Get the current compilation/type system
205215
/// </summary>
@@ -316,7 +326,9 @@ public void UpdateSettings(DecompilerSettings settings)
316326
try
317327
{
318328
if (!IsLoaded) return;
319-
_decompiler = new CSharpDecompiler(_peFile!, _resolver!, settings);
329+
var settingsSnapshot = CloneSettings(settings);
330+
_settings = CloneSettings(settingsSnapshot);
331+
_decompiler = new CSharpDecompiler(_peFile!, _resolver!, settingsSnapshot);
320332
Interlocked.Increment(ref _contextVersion);
321333
}
322334
finally
@@ -512,10 +524,39 @@ private static PEFile CreatePEFile(string assemblyPath)
512524
return new PEFile(assemblyPath, AssemblyLoadOptions, MetadataReaderOptions.Default);
513525
}
514526

527+
private static DecompilerSettings CreateDefaultDecompilerSettings()
528+
{
529+
return new DecompilerSettings
530+
{
531+
UsingDeclarations = true,
532+
ShowXmlDocumentation = true,
533+
NamedArguments = true,
534+
MakeAssignmentExpressions = true,
535+
AlwaysUseBraces = true,
536+
RemoveDeadCode = true,
537+
IntroduceIncrementAndDecrement = true
538+
};
539+
}
540+
541+
private static DecompilerSettings CloneSettings(DecompilerSettings settings)
542+
{
543+
return new DecompilerSettings
544+
{
545+
UsingDeclarations = settings.UsingDeclarations,
546+
ShowXmlDocumentation = settings.ShowXmlDocumentation,
547+
NamedArguments = settings.NamedArguments,
548+
MakeAssignmentExpressions = settings.MakeAssignmentExpressions,
549+
AlwaysUseBraces = settings.AlwaysUseBraces,
550+
RemoveDeadCode = settings.RemoveDeadCode,
551+
IntroduceIncrementAndDecrement = settings.IntroduceIncrementAndDecrement
552+
};
553+
}
554+
515555
private void DisposeContext()
516556
{
517557
Interlocked.Increment(ref _contextVersion);
518558
_decompiler = null;
559+
_settings = null;
519560
_compilation = null;
520561
_peFile?.Dispose();
521562
_peFile = null;

Services/DecompilerWorkspace.cs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public WorkspaceContextInfo LoadAssembly(WorkspaceLoadRequest request)
4646
}
4747

4848
_sessionsByAlias[contextAlias] = session;
49-
_aliasByMvid[session.ContextManager.Mvid!] = contextAlias;
49+
AddSessionMappings(session);
5050

5151
if (request.MakeCurrent || CurrentContextAlias == null)
5252
{
@@ -419,12 +419,44 @@ private static string GetDefaultRegistryPath()
419419
private void RemoveSessionMappings(DecompilerSession session)
420420
{
421421
_sessionsByAlias.Remove(session.ContextAlias);
422-
if (session.ContextManager.Mvid != null)
423-
_aliasByMvid.Remove(session.ContextManager.Mvid);
422+
423+
var mvid = session.ContextManager.Mvid;
424+
if (mvid != null &&
425+
_aliasByMvid.TryGetValue(mvid, out var mappedAlias) &&
426+
string.Equals(mappedAlias, session.ContextAlias, StringComparison.OrdinalIgnoreCase))
427+
{
428+
_aliasByMvid.Remove(mvid);
429+
430+
var replacement = FindReplacementSessionForMvid(mvid);
431+
if (replacement != null)
432+
_aliasByMvid[mvid] = replacement.ContextAlias;
433+
}
424434

425435
if (string.Equals(CurrentContextAlias, session.ContextAlias, StringComparison.OrdinalIgnoreCase))
426436
CurrentContextAlias = null;
427437
}
438+
439+
private void AddSessionMappings(DecompilerSession session)
440+
{
441+
var mvid = session.ContextManager.Mvid;
442+
if (mvid != null && !_aliasByMvid.ContainsKey(mvid))
443+
_aliasByMvid[mvid] = session.ContextAlias;
444+
}
445+
446+
private DecompilerSession? FindReplacementSessionForMvid(string mvid)
447+
{
448+
if (CurrentContextAlias != null &&
449+
_sessionsByAlias.TryGetValue(CurrentContextAlias, out var currentSession) &&
450+
string.Equals(currentSession.ContextManager.Mvid, mvid, StringComparison.OrdinalIgnoreCase))
451+
{
452+
return currentSession;
453+
}
454+
455+
return _sessionsByAlias.Values
456+
.Where(candidate => string.Equals(candidate.ContextManager.Mvid, mvid, StringComparison.OrdinalIgnoreCase))
457+
.OrderBy(candidate => candidate.ContextAlias, StringComparer.OrdinalIgnoreCase)
458+
.FirstOrDefault();
459+
}
428460
}
429461

430462
public sealed class DecompilerSession : IDisposable
@@ -481,14 +513,23 @@ public WorkspaceContextInfo ToContextInfo(bool isCurrent)
481513
ContextAlias = ContextAlias,
482514
Mvid = ContextManager.Mvid!,
483515
AssemblyPath = ContextManager.AssemblyPath!,
484-
LoadedAtUnix = ContextManager.LoadedAtUtc?.Ticks,
516+
LoadedAtUnix = ToUnixTimeSeconds(ContextManager.LoadedAtUtc),
485517
TypeCount = ContextManager.TypeCount,
486518
MethodCount = ContextManager.GetAllTypes().Sum(type => type.Methods.Count()),
487519
NamespaceCount = ContextManager.NamespaceCount,
488520
IsCurrent = isCurrent
489521
};
490522
}
491523

524+
private static long? ToUnixTimeSeconds(DateTime? utc)
525+
{
526+
if (!utc.HasValue)
527+
return null;
528+
529+
var value = DateTime.SpecifyKind(utc.Value, DateTimeKind.Utc);
530+
return new DateTimeOffset(value).ToUnixTimeSeconds();
531+
}
532+
492533
public void Dispose()
493534
{
494535
if (_disposed)

Services/IlAnalysisService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public static IEnumerable<IlInstructionInfo> ReadInstructions(byte[] il, Metadat
7474

7575
object? operand = null;
7676
int? operandToken = null;
77+
string? operandTokenHex = null;
7778
string? operandKind = null;
7879
string? operandSummary = null;
7980

@@ -101,11 +102,13 @@ public static IEnumerable<IlInstructionInfo> ReadInstructions(byte[] il, Metadat
101102
position += 4;
102103
operand = token;
103104
operandToken = token;
105+
operandTokenHex = $"0x{token:X8}";
104106
operandKind = opCode.OperandType.ToString();
105107
operandSummary = FormatMetadataToken(metadata, token);
106108
break;
107109
case OperandType.InlineSig:
108110
operandToken = BitConverter.ToInt32(il, position);
111+
operandTokenHex = $"0x{operandToken.Value:X8}";
109112
position += 4;
110113
operand = operandToken;
111114
operandKind = "signature";
@@ -116,6 +119,7 @@ public static IEnumerable<IlInstructionInfo> ReadInstructions(byte[] il, Metadat
116119
position += 4;
117120
operand = stringToken;
118121
operandToken = stringToken;
122+
operandTokenHex = $"0x{stringToken:X8}";
119123
operandKind = "string";
120124
operandSummary = Quote(metadata.GetUserString(MetadataTokens.UserStringHandle(stringToken)));
121125
break;
@@ -185,6 +189,7 @@ public static IEnumerable<IlInstructionInfo> ReadInstructions(byte[] il, Metadat
185189
OpCode: opCode.Name ?? opCode.ToString() ?? "unknown",
186190
Operand: operand,
187191
OperandToken: operandToken,
192+
OperandTokenHex: operandTokenHex,
188193
OperandKind: operandKind,
189194
OperandSummary: operandSummary);
190195
}
@@ -300,5 +305,6 @@ public sealed record IlInstructionInfo(
300305
string OpCode,
301306
object? Operand,
302307
int? OperandToken,
308+
string? OperandTokenHex,
303309
string? OperandKind,
304310
string? OperandSummary);

0 commit comments

Comments
 (0)