#
tokens: 48011/50000 37/48 files (page 1/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 4. Use http://codebase.md/kooshi/sharptoolsmcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .github
│   └── copilot-instructions.md
├── .gitignore
├── LICENSE
├── Prompts
│   ├── github-copilot-sharptools.prompt
│   └── identity.prompt
├── README.md
├── SharpTools.sln
├── SharpTools.SseServer
│   ├── Program.cs
│   └── SharpTools.SseServer.csproj
├── SharpTools.StdioServer
│   ├── Program.cs
│   └── SharpTools.StdioServer.csproj
└── SharpTools.Tools
    ├── Extensions
    │   ├── ServiceCollectionExtensions.cs
    │   └── SyntaxTreeExtensions.cs
    ├── GlobalUsings.cs
    ├── Interfaces
    │   ├── ICodeAnalysisService.cs
    │   ├── ICodeModificationService.cs
    │   ├── IComplexityAnalysisService.cs
    │   ├── IDocumentOperationsService.cs
    │   ├── IEditorConfigProvider.cs
    │   ├── IFuzzyFqnLookupService.cs
    │   ├── IGitService.cs
    │   ├── ISemanticSimilarityService.cs
    │   ├── ISolutionManager.cs
    │   └── ISourceResolutionService.cs
    ├── Mcp
    │   ├── ContextInjectors.cs
    │   ├── ErrorHandlingHelpers.cs
    │   ├── Prompts.cs
    │   ├── ToolHelpers.cs
    │   └── Tools
    │       ├── AnalysisTools.cs
    │       ├── DocumentTools.cs
    │       ├── MemberAnalysisHelper.cs
    │       ├── MiscTools.cs
    │       ├── ModificationTools.cs
    │       ├── PackageTools.cs
    │       └── SolutionTools.cs
    ├── Services
    │   ├── ClassSemanticFeatures.cs
    │   ├── ClassSimilarityResult.cs
    │   ├── CodeAnalysisService.cs
    │   ├── CodeModificationService.cs
    │   ├── ComplexityAnalysisService.cs
    │   ├── DocumentOperationsService.cs
    │   ├── EditorConfigProvider.cs
    │   ├── EmbeddedSourceReader.cs
    │   ├── FuzzyFqnLookupService.cs
    │   ├── GitService.cs
    │   ├── LegacyNuGetPackageReader.cs
    │   ├── MethodSemanticFeatures.cs
    │   ├── MethodSimilarityResult.cs
    │   ├── NoOpGitService.cs
    │   ├── PathInfo.cs
    │   ├── SemanticSimilarityService.cs
    │   ├── SolutionManager.cs
    │   └── SourceResolutionService.cs
    └── SharpTools.Tools.csproj
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # app specific user settings
 2 | .claude
 3 | .gemini
 4 | 
 5 | # Prerequisites
 6 | *.vsconfig
 7 | 
 8 | # User-specific files
 9 | *.rsuser
10 | *.suo
11 | *.user
12 | *.userosscache
13 | *.sln.docstates
14 | 
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | [Ww][Ii][Nn]32/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 | [Ll]ogs/
30 | 
31 | # Visual Studio cache files
32 | .vs/
33 | *.VC.db
34 | *.VC.VC.opendb
35 | 
36 | # Rider
37 | .idea/
38 | 
39 | # Test results
40 | [Tt]est[Rr]esult*/
41 | [Bb]uild[Ll]og.*
42 | *.VisualState.xml
43 | TestResult.xml
44 | *.trx
45 | 
46 | # Dotnet files
47 | project.lock.json
48 | project.assets.json
49 | *.nuget.props
50 | *.nuget.targets
51 | 
52 | # Secrets
53 | secrets.json
54 | 
55 | # IDE
56 | *.code-workspace
57 | 
58 | # OS generated files
59 | ehthumbs.db
60 | Thumbs.db
61 | DS_Store
62 | 
63 | publish/
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
  1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories
  2 | root = true
  3 | 
  4 | # C# files
  5 | [*.cs]
  6 | 
  7 | #### Core EditorConfig Options ####
  8 | 
  9 | # Indentation and spacing
 10 | indent_size = 4
 11 | indent_style = space
 12 | tab_width = 4
 13 | 
 14 | #### .NET Code Actions ####
 15 | 
 16 | # Type members
 17 | dotnet_hide_advanced_members = false
 18 | dotnet_member_insertion_location = with_other_members_of_the_same_kind
 19 | dotnet_property_generation_behavior = prefer_throwing_properties
 20 | 
 21 | # Symbol search
 22 | dotnet_search_reference_assemblies = true
 23 | 
 24 | #### .NET Coding Conventions ####
 25 | 
 26 | # Organize usings
 27 | dotnet_separate_import_directive_groups = false
 28 | dotnet_sort_system_directives_first = true
 29 | file_header_template = unset
 30 | 
 31 | # this. and Me. preferences
 32 | dotnet_style_qualification_for_event = false:suggestion
 33 | dotnet_style_qualification_for_field = false
 34 | dotnet_style_qualification_for_method = false:suggestion
 35 | dotnet_style_qualification_for_property = false:suggestion
 36 | 
 37 | # Language keywords vs BCL types preferences
 38 | dotnet_style_predefined_type_for_locals_parameters_members = true
 39 | dotnet_style_predefined_type_for_member_access = true
 40 | 
 41 | # Parentheses preferences
 42 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
 43 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
 44 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary
 45 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
 46 | 
 47 | # Modifier preferences
 48 | dotnet_style_require_accessibility_modifiers = for_non_interface_members
 49 | 
 50 | # Expression-level preferences
 51 | dotnet_prefer_system_hash_code = true
 52 | dotnet_style_coalesce_expression = true
 53 | dotnet_style_collection_initializer = true
 54 | dotnet_style_explicit_tuple_names = true:warning
 55 | dotnet_style_namespace_match_folder = true
 56 | dotnet_style_null_propagation = true
 57 | dotnet_style_object_initializer = true
 58 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
 59 | dotnet_style_prefer_auto_properties = true:suggestion
 60 | dotnet_style_prefer_collection_expression = when_types_loosely_match
 61 | dotnet_style_prefer_compound_assignment = true
 62 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
 63 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion
 64 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
 65 | dotnet_style_prefer_inferred_anonymous_type_member_names = true
 66 | dotnet_style_prefer_inferred_tuple_names = true
 67 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
 68 | dotnet_style_prefer_simplified_boolean_expressions = true
 69 | dotnet_style_prefer_simplified_interpolation = true
 70 | 
 71 | # Field preferences
 72 | dotnet_style_readonly_field = true
 73 | 
 74 | # Parameter preferences
 75 | dotnet_code_quality_unused_parameters = all
 76 | 
 77 | # Suppression preferences
 78 | dotnet_remove_unnecessary_suppression_exclusions = none
 79 | 
 80 | # New line preferences
 81 | dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
 82 | dotnet_style_allow_statement_immediately_after_block_experimental = true
 83 | 
 84 | #### C# Coding Conventions ####
 85 | 
 86 | # var preferences
 87 | csharp_style_var_elsewhere = false
 88 | csharp_style_var_for_built_in_types = false:suggestion
 89 | csharp_style_var_when_type_is_apparent = false
 90 | 
 91 | # Expression-bodied members
 92 | csharp_style_expression_bodied_accessors = true:suggestion
 93 | csharp_style_expression_bodied_constructors = false
 94 | csharp_style_expression_bodied_indexers = true:suggestion
 95 | csharp_style_expression_bodied_lambdas = true:suggestion
 96 | csharp_style_expression_bodied_local_functions = false
 97 | csharp_style_expression_bodied_methods = when_on_single_line
 98 | csharp_style_expression_bodied_operators = true:suggestion
 99 | csharp_style_expression_bodied_properties = true:suggestion
100 | 
101 | # Pattern matching preferences
102 | csharp_style_pattern_matching_over_as_with_null_check = true
103 | csharp_style_pattern_matching_over_is_with_cast_check = true
104 | csharp_style_prefer_extended_property_pattern = true
105 | csharp_style_prefer_not_pattern = true
106 | csharp_style_prefer_pattern_matching = true:suggestion
107 | csharp_style_prefer_switch_expression = true
108 | 
109 | # Null-checking preferences
110 | csharp_style_conditional_delegate_call = true
111 | 
112 | # Modifier preferences
113 | csharp_prefer_static_anonymous_function = true
114 | csharp_prefer_static_local_function = true
115 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
116 | csharp_style_prefer_readonly_struct = true
117 | csharp_style_prefer_readonly_struct_member = true
118 | 
119 | # Code-block preferences
120 | csharp_prefer_braces = true
121 | csharp_prefer_simple_using_statement = true
122 | csharp_prefer_system_threading_lock = true
123 | csharp_style_namespace_declarations = block_scoped
124 | csharp_style_prefer_method_group_conversion = true:suggestion
125 | csharp_style_prefer_primary_constructors = true
126 | csharp_style_prefer_top_level_statements = true
127 | 
128 | # Expression-level preferences
129 | csharp_prefer_simple_default_expression = true
130 | csharp_style_deconstructed_variable_declaration = true
131 | csharp_style_implicit_object_creation_when_type_is_apparent = true
132 | csharp_style_inlined_variable_declaration = true
133 | csharp_style_prefer_index_operator = true
134 | csharp_style_prefer_local_over_anonymous_function = true:warning
135 | csharp_style_prefer_null_check_over_type_check = true:warning
136 | csharp_style_prefer_range_operator = true
137 | csharp_style_prefer_tuple_swap = true
138 | csharp_style_prefer_utf8_string_literals = true
139 | csharp_style_throw_expression = true
140 | csharp_style_unused_value_assignment_preference = discard_variable
141 | csharp_style_unused_value_expression_statement_preference = discard_variable
142 | 
143 | # 'using' directive preferences
144 | csharp_using_directive_placement = outside_namespace:suggestion
145 | 
146 | # New line preferences
147 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
148 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
149 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
150 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
151 | csharp_style_allow_embedded_statements_on_same_line_experimental = true
152 | 
153 | #### C# Formatting Rules ####
154 | 
155 | # New line preferences
156 | csharp_new_line_before_catch = false
157 | csharp_new_line_before_else = false
158 | csharp_new_line_before_finally = false
159 | csharp_new_line_before_members_in_anonymous_types = true
160 | csharp_new_line_before_members_in_object_initializers = true
161 | csharp_new_line_before_open_brace = none
162 | csharp_new_line_between_query_expression_clauses = true
163 | 
164 | # Indentation preferences
165 | csharp_indent_block_contents = true
166 | csharp_indent_braces = false
167 | csharp_indent_case_contents = true
168 | csharp_indent_case_contents_when_block = false
169 | csharp_indent_labels = no_change
170 | csharp_indent_switch_labels = true
171 | 
172 | # Space preferences
173 | csharp_space_after_cast = false
174 | csharp_space_after_colon_in_inheritance_clause = true
175 | csharp_space_after_comma = true
176 | csharp_space_after_dot = false
177 | csharp_space_after_keywords_in_control_flow_statements = true
178 | csharp_space_after_semicolon_in_for_statement = true
179 | csharp_space_around_binary_operators = before_and_after
180 | csharp_space_around_declaration_statements = false
181 | csharp_space_before_colon_in_inheritance_clause = true
182 | csharp_space_before_comma = false
183 | csharp_space_before_dot = false
184 | csharp_space_before_open_square_brackets = false
185 | csharp_space_before_semicolon_in_for_statement = false
186 | csharp_space_between_empty_square_brackets = false
187 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
188 | csharp_space_between_method_call_name_and_opening_parenthesis = false
189 | csharp_space_between_method_call_parameter_list_parentheses = false
190 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
191 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
192 | csharp_space_between_method_declaration_parameter_list_parentheses = false
193 | csharp_space_between_parentheses = false
194 | csharp_space_between_square_brackets = false
195 | 
196 | # Wrapping preferences
197 | csharp_preserve_single_line_blocks = true
198 | csharp_preserve_single_line_statements = true
199 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # SharpTools: Roslyn Powered C# Analysis & Modification MCP Server
  2 | 
  3 | SharpTools is a robust service designed to empower AI agents with advanced capabilities for understanding, analyzing, and modifying C# codebases. It leverages the .NET Compiler Platform (Roslyn) to provide deep static analysis and precise code manipulation, going far beyond simple text-based operations.
  4 | 
  5 | SharpTools is designed to give AI the same insights and tools a human developer relies on, leading to more intelligent and reliable code assistance. It is effectively a simple IDE, made for an AI user.
  6 | 
  7 | Due to the comprehensive nature of the suite, it can almost be used completely standalone for editing existing C# solutions. If you use the SSE server and port forward your router, I think it's even possible to have Claude's web chat ui connect to this and have it act as a full coding assistant.
  8 | 
  9 | ## Prompts
 10 | 
 11 | The included [Identity Prompt](Prompts/identity.prompt) is my personal C# coding assistant prompt, and it works well in combination with this suite. You're welcome to use it as is, modify it to match your preferences, or omit it entirely.
 12 | 
 13 | In VS Code, set it as your `copilot-instructions.md` to have it included in every interaction.
 14 | 
 15 | The [Tool Use Prompt](Prompts/github-copilot-sharptools.prompt) is fomulated specifically for Github Copilot's Agent mode. It overrides specific sections within the Copilot Agent System Prompt so that it avoids the built in tools.
 16 | 
 17 | It is available as an MCP prompt as well, so within Copilot, you just need to type `/mcp`, and it should show up as an option.
 18 | 
 19 | Something similar will be necessary for other coding assistants to prevent them from using their own default editing tools.
 20 | 
 21 | I recommend crafting custom tool use prompts for each agent you use this with, based on their [individual system prompts](https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools).
 22 | 
 23 | ## Note
 24 | 
 25 | This is a personal project, slapped together over a few weeks, and built in no small part by its own tools. It generally works well as it is, but the code is still fairly ugly, and some of the features are still quirky (like removing newlines before and after overwritten members).
 26 | 
 27 | I intend to maintain and improve it for as long as I am using it, and I welcome feedback and contributions as a part of that.
 28 | 
 29 | ## Features
 30 | 
 31 | *   **Dynamic Project Structure Mapping:** Generates a "map" of the solution, detailing namespaces and types, with complexity-adjusted resolution.
 32 | *   **Contextual Navigation Aids:** Provides simplified call graphs and dependency trees for local code understanding.
 33 | *   **Token Efficient Operation** Designed to provide only the highest signal context at every step to keep your agent on track longer without being overwhelmed or requiring summarization.
 34 |     *   All indentation is omitted in returned code, saving roughly 10% of tokens without affecting performance on the smartest models.
 35 |     *   FQN based navigation means the agent rarely needs to read unrelated code.
 36 | *   **FQN Fuzzy Matching:** Intelligently resolves potentially imprecise or incomplete Fully Qualified Names (FQNs) to exact Roslyn symbols.
 37 | *   **Comprehensive Source Resolution:** Retrieves source code for symbols from:
 38 |     *   Local solution files.
 39 |     *   External libraries via SourceLink.
 40 |     *   Embedded PDBs.
 41 |     *   Decompilation (ILSpy-based) as a fallback.
 42 | *   **Precise, Roslyn-Based Modifications:** Enables surgical code changes (add/overwrite/rename/move members, find/replace) rather than simple text manipulation.
 43 | *   **Automated Git Integration:**
 44 |     *   Creates dedicated, timestamped `sharptools/` branches for all modifications.
 45 |     *   Automatically commits every code change with a descriptive message.
 46 |     *   Offers a Git-powered `Undo` for the last modification.
 47 | *   **Concise AI Feedback Loop:**
 48 |     *   Confirms changes with precise diffs instead of full code blocks.
 49 |     *   Provides immediate, in-tool compilation error reports after modifications.
 50 | *   **Proactive Code Quality Analysis:**
 51 |     *   Detects and warns about high code complexity (cyclomatic, cognitive).
 52 |     *   Identifies semantically similar code to flag potential duplicates upon member addition.
 53 | *   **Broad Project Support:**
 54 |     *   Runs on Windows and Linux (and probably Mac)
 55 |     *   Can analyze projects targeting any .NET version, from Framework to Core to 5+
 56 |     *   Compatible with both modern SDK-style and legacy C# project formats.
 57 |     *   Respects `.editorconfig` settings for consistent code formatting.
 58 | *   **MCP Server Interface:** Exposes tools via Model Context Protocol (MCP) through:
 59 |     *   Server-Sent Events (SSE) for remote clients.
 60 |     *   Standard I/O (Stdio) for local process communication.
 61 | 
 62 | ## Exposed Tools
 63 | 
 64 | SharpTools exposes a variety of "SharpTool_*" functions via MCP. Here's a brief overview categorized by their respective service files:
 65 | 
 66 | ### Solution Tools
 67 | 
 68 | *   `SharpTool_LoadSolution`: Initializes the workspace with a given `.sln` file. This is the primary entry point.
 69 | *   `SharpTool_LoadProject`: Provides a detailed structural overview of a specific project within the loaded solution, including namespaces and types, to aid AI understanding of the project's layout.
 70 | 
 71 | ### Analysis Tools
 72 | 
 73 | *   `SharpTool_GetMembers`: Lists members (methods, properties, etc.) of a type, including signatures and XML documentation.
 74 | *   `SharpTool_ViewDefinition`: Displays the source code of a symbol (class, method, etc.), including contextual information like call graphs or type references.
 75 | *   `SharpTool_ListImplementations`: Finds all implementations of an interface/abstract method or derived classes of a base class.
 76 | *   `SharpTool_FindReferences`: Locates all usages of a symbol across the solution, providing contextual code snippets.
 77 | *   `SharpTool_SearchDefinitions`: Performs a regex-based search across symbol declarations and signatures in both source code and compiled assemblies.
 78 | *   `SharpTool_ManageUsings`: Reads or overwrites using directives in a document.
 79 | *   `SharpTool_ManageAttributes`: Reads or overwrites attributes on a specific declaration.
 80 | *   `SharpTool_AnalyzeComplexity`: Performs complexity analysis (cyclomatic, cognitive, coupling, etc.) on methods, classes, or projects.
 81 | *   ~(Disabled) `SharpTool_GetAllSubtypes`: Recursively lists all nested members of a type.~
 82 | *   ~(Disabled) `SharpTool_ViewInheritanceChain`: Shows the inheritance hierarchy for a type.~
 83 | *   ~(Disabled) `SharpTool_ViewCallGraph`: Displays incoming and outgoing calls for a method.~
 84 | *   ~(Disabled) `SharpTool_FindPotentialDuplicates`: Finds semantically similar methods or classes.~
 85 | 
 86 | ### Document Tools
 87 | 
 88 | *   `SharpTool_ReadRawFromRoslynDocument`: Reads the raw content of a file (indentation omitted).
 89 | *   `SharpTool_CreateRoslynDocument`: Creates a new file with specified content.
 90 | *   `SharpTool_OverwriteRoslynDocument`: Overwrites an existing file with new content.
 91 | *   `SharpTool_ReadTypesFromRoslynDocument`: Lists all types and their members defined within a specific source file.
 92 | 
 93 | ### Modification Tools
 94 | 
 95 | *   `SharpTool_AddMember`: Adds a new member (method, property, field, nested type, etc.) to a specified type.
 96 | *   `SharpTool_OverwriteMember`: Replaces the definition of an existing member or type with new code, or deletes it.
 97 | *   `SharpTool_RenameSymbol`: Renames a symbol and updates all its references throughout the solution.
 98 | *   `SharpTool_FindAndReplace`: Performs regex-based find and replace operations within a specified symbol's declaration or across files matching a glob pattern.
 99 | *   `SharpTool_MoveMember`: Moves a member from one type/namespace to another.
100 | *   `SharpTool_Undo`: Reverts the last applied change using Git integration.
101 | *   ~(Disabled) `SharpTool_ReplaceAllReferences`: Replaces all references to a symbol with specified C# code.~
102 | 
103 | ### Package Tools
104 | 
105 | *   ~(Disabled) `SharpTool_AddOrModifyNugetPackage`: Adds or updates a NuGet package reference in a project file.~
106 | 
107 | ### Misc Tools
108 | 
109 | *   `SharpTool_RequestNewTool`: Allows the AI to request new tools or features, logging the request for human review.
110 | 
111 | ## Prerequisites
112 | 
113 | *   .NET 8+ SDK for running the server
114 | *   The .NET SDK of your target solution
115 | 
116 | ## Building
117 | 
118 | To build the entire solution:
119 | ```bash
120 | dotnet build SharpTools.sln
121 | ```
122 | This will build all services and server applications.
123 | 
124 | ## Running the Servers
125 | 
126 | ### SSE Server (HTTP)
127 | 
128 | The SSE server hosts the tools on an HTTP endpoint.
129 | 
130 | ```bash
131 | # Navigate to the SseServer project directory
132 | cd SharpTools.SseServer
133 | 
134 | # Run with default options (port 3001)
135 | dotnet run
136 | 
137 | # Run with specific options
138 | dotnet run -- --port 3005 --log-file ./logs/mcp-sse-server.log --log-level Debug
139 | ```
140 | Key Options:
141 | *   `--port <number>`: Port to listen on (default: 3001).
142 | *   `--log-file <path>`: Path to a log file.
143 | *   `--log-level <level>`: Minimum log level (Verbose, Debug, Information, Warning, Error, Fatal).
144 | *   `--load-solution <path>`: Path to a `.sln` file to load on startup. Useful for manual testing. It is recommended to let the AI run the LoadSolution tool instead, as it returns some useful information.
145 | *   `--build-configuration <config>`: Build configuration to use when loading the solution (e.g., `Debug`, `Release`).
146 | *   `--disable-git`: Disables all Git integration features.
147 | 
148 | ### Stdio Server
149 | 
150 | The Stdio server communicates over standard input/output.
151 | 
152 | Configure it in your MCP client of choice.
153 | VSCode Copilot example:
154 | 
155 | ```json
156 | "mcp": {
157 |     "servers": {
158 |         "SharpTools": {
159 |             "type": "stdio",
160 |             "command": "/path/to/repo/SharpToolsMCP/SharpTools.StdioServer/bin/Debug/net8.0/SharpTools.StdioServer",
161 |             "args": [
162 |                 "--log-directory",
163 |                 "/var/log/sharptools/",
164 |                 "--log-level",
165 |                 "Debug",
166 |             ]
167 |         }
168 |     }
169 | },
170 | ```
171 | Key Options:
172 | *   `--log-directory <path>`: Directory to store log files.
173 | *   `--log-level <level>`: Minimum log level.
174 | *   `--load-solution <path>`: Path to a `.sln` file to load on startup. Useful for manual testing. It is recommended to let the AI run the LoadSolution tool instead, as it returns some useful information.
175 | *   `--build-configuration <config>`: Build configuration to use when loading the solution (e.g., `Debug`, `Release`).
176 | *   `--disable-git`: Disables all Git integration features.
177 | 
178 | ## Contributing
179 | 
180 | Contributions are welcome! Please feel free to submit pull requests or open issues.
181 | 
182 | ## License
183 | 
184 | This project is licensed under the MIT License - see the LICENSE file for details.
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/ClassSimilarityResult.cs:
--------------------------------------------------------------------------------

```csharp
1 | using System.Collections.Generic;
2 | 
3 | namespace SharpTools.Tools.Services;
4 | 
5 | public record ClassSimilarityResult(
6 |     List<ClassSemanticFeatures> SimilarClasses,
7 |     double AverageSimilarityScore
8 | );
9 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/IEditorConfigProvider.cs:
--------------------------------------------------------------------------------

```csharp
1 | namespace SharpTools.Tools.Interfaces;
2 | 
3 | public interface IEditorConfigProvider
4 | {
5 |     Task InitializeAsync(string solutionDirectory, CancellationToken cancellationToken);
6 |     string? GetRootEditorConfigPath();
7 |     // OptionSet retrieval is primarily handled by Document.GetOptionsAsync(),
8 |     // but this provider can offer workspace-wide defaults or specific lookups if needed.
9 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/MethodSimilarityResult.cs:
--------------------------------------------------------------------------------

```csharp
 1 | 
 2 | using System.Collections.Generic;
 3 | 
 4 | namespace SharpTools.Tools.Services {
 5 |     public class MethodSimilarityResult {
 6 |         public List<MethodSemanticFeatures> SimilarMethods { get; }
 7 |         public double AverageSimilarityScore { get; } // Or some other metric
 8 | 
 9 |         public MethodSimilarityResult(List<MethodSemanticFeatures> similarMethods, double averageSimilarityScore) {
10 |             SimilarMethods = similarMethods;
11 |             AverageSimilarityScore = averageSimilarityScore;
12 |         }
13 |     }
14 | }
15 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/ISemanticSimilarityService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | 
 2 | using System.Collections.Generic;
 3 | using System.Threading;
 4 | using System.Threading.Tasks;
 5 | 
 6 | namespace SharpTools.Tools.Interfaces {
 7 | 
 8 |     public interface ISemanticSimilarityService {
 9 |         Task<List<MethodSimilarityResult>> FindSimilarMethodsAsync(
10 |             double similarityThreshold,
11 |             CancellationToken cancellationToken);
12 | 
13 |         Task<List<ClassSimilarityResult>> FindSimilarClassesAsync(
14 |             double similarityThreshold,
15 |             CancellationToken cancellationToken);
16 |     }
17 | }
18 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/IComplexityAnalysisService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Microsoft.CodeAnalysis;
 2 | using System.Collections.Generic;
 3 | using System.Threading;
 4 | using System.Threading.Tasks;
 5 | 
 6 | namespace SharpTools.Tools.Interfaces;
 7 | 
 8 | public interface IComplexityAnalysisService {
 9 |     Task AnalyzeMethodAsync(
10 |         IMethodSymbol methodSymbol,
11 |         Dictionary<string, object> metrics,
12 |         List<string> recommendations,
13 |         CancellationToken cancellationToken);
14 | 
15 |     Task AnalyzeTypeAsync(
16 |         INamedTypeSymbol typeSymbol,
17 |         Dictionary<string, object> metrics,
18 |         List<string> recommendations,
19 |         bool includeGeneratedCode,
20 |         CancellationToken cancellationToken);
21 | 
22 |     Task AnalyzeProjectAsync(
23 |         Project project,
24 |         Dictionary<string, object> metrics,
25 |         List<string> recommendations,
26 |         bool includeGeneratedCode,
27 |         CancellationToken cancellationToken);
28 | }
29 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Extensions/SyntaxTreeExtensions.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Microsoft.CodeAnalysis;
 2 | using System.Linq;
 3 | 
 4 | namespace SharpTools.Tools.Extensions;
 5 | 
 6 | public static class SyntaxTreeExtensions
 7 | {
 8 |     public static Project GetRequiredProject(this SyntaxTree tree, Solution solution)
 9 |     {
10 |         var projectIds = solution.Projects
11 |             .Where(p => p.Documents.Any(d => d.FilePath == tree.FilePath))
12 |             .Select(p => p.Id)
13 |             .ToList();
14 | 
15 |         if (projectIds.Count == 0)
16 |             throw new InvalidOperationException($"Could not find project containing file {tree.FilePath}");
17 |         
18 |         if (projectIds.Count > 1)
19 |             throw new InvalidOperationException($"File {tree.FilePath} belongs to multiple projects");
20 |         
21 |         var project = solution.GetProject(projectIds[0]);
22 |         if (project == null)
23 |             throw new InvalidOperationException($"Could not get project with ID {projectIds[0]}");
24 |         
25 |         return project;
26 |     }
27 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/ClassSemanticFeatures.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using System.Collections.Generic;
 2 | 
 3 | namespace SharpTools.Tools.Services;
 4 | 
 5 | public record ClassSemanticFeatures(
 6 |     string FullyQualifiedClassName,
 7 |     string FilePath,
 8 |     int StartLine,
 9 |     string ClassName,
10 |     string? BaseClassName,
11 |     List<string> ImplementedInterfaceNames,
12 |     int PublicMethodCount,
13 |     int ProtectedMethodCount,
14 |     int PrivateMethodCount,
15 |     int StaticMethodCount,
16 |     int AbstractMethodCount,
17 |     int VirtualMethodCount,
18 |     int PropertyCount,
19 |     int ReadOnlyPropertyCount,
20 |     int StaticPropertyCount,
21 |     int FieldCount,
22 |     int StaticFieldCount,
23 |     int ReadonlyFieldCount,
24 |     int ConstFieldCount,
25 |     int EventCount,
26 |     int NestedClassCount,
27 |     int NestedStructCount,
28 |     int NestedEnumCount,
29 |     int NestedInterfaceCount,
30 |     double AverageMethodComplexity,
31 |     HashSet<string> DistinctReferencedExternalTypeFqns,
32 |     HashSet<string> DistinctUsedNamespaceFqns,
33 |     int TotalLinesOfCode,
34 |     List<MethodSemanticFeatures> MethodFeatures // Added for inter-class method similarity
35 | );
36 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/IGitService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Interfaces;
 2 | public interface IGitService {
 3 |     Task<bool> IsRepositoryAsync(string solutionPath, CancellationToken cancellationToken = default);
 4 |     Task<bool> IsOnSharpToolsBranchAsync(string solutionPath, CancellationToken cancellationToken = default);
 5 |     Task EnsureSharpToolsBranchAsync(string solutionPath, CancellationToken cancellationToken = default);
 6 |     Task CommitChangesAsync(string solutionPath, IEnumerable<string> changedFilePaths, string commitMessage, CancellationToken cancellationToken = default);
 7 |     Task<(bool success, string diff)> RevertLastCommitAsync(string solutionPath, CancellationToken cancellationToken = default);
 8 |     Task<string> GetBranchOriginCommitAsync(string solutionPath, CancellationToken cancellationToken = default);
 9 |     Task<string> CreateUndoBranchAsync(string solutionPath, CancellationToken cancellationToken = default);
10 |     Task<string> GetDiffAsync(string solutionPath, string oldCommitSha, string newCommitSha, CancellationToken cancellationToken = default);
11 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/ICodeAnalysisService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Interfaces;
 2 | public interface ICodeAnalysisService {
 3 |     Task<IEnumerable<ISymbol>> FindImplementationsAsync(ISymbol symbol, CancellationToken cancellationToken);
 4 |     Task<IEnumerable<ISymbol>> FindOverridesAsync(ISymbol symbol, CancellationToken cancellationToken);
 5 |     Task<IEnumerable<ReferencedSymbol>> FindReferencesAsync(ISymbol symbol, CancellationToken cancellationToken);
 6 |     Task<IEnumerable<INamedTypeSymbol>> FindDerivedClassesAsync(INamedTypeSymbol typeSymbol, CancellationToken cancellationToken);
 7 |     Task<IEnumerable<INamedTypeSymbol>> FindDerivedInterfacesAsync(INamedTypeSymbol typeSymbol, CancellationToken cancellationToken);
 8 |     Task<IEnumerable<SymbolCallerInfo>> FindCallersAsync(ISymbol symbol, CancellationToken cancellationToken);
 9 |     Task<IEnumerable<ISymbol>> FindOutgoingCallsAsync(IMethodSymbol methodSymbol, CancellationToken cancellationToken);
10 |     Task<string?> GetXmlDocumentationAsync(ISymbol symbol, CancellationToken cancellationToken);
11 |     Task<HashSet<string>> FindReferencedTypesAsync(INamedTypeSymbol typeSymbol, CancellationToken cancellationToken);
12 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/ISolutionManager.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Interfaces;
 2 | 
 3 | public interface ISolutionManager : IDisposable {
 4 |     [MemberNotNullWhen(true, nameof(CurrentWorkspace), nameof(CurrentSolution))]
 5 |     bool IsSolutionLoaded { get; }
 6 |     MSBuildWorkspace? CurrentWorkspace { get; }
 7 |     Solution? CurrentSolution { get; }
 8 |     
 9 | 
10 |     Task LoadSolutionAsync(string solutionPath, CancellationToken cancellationToken);
11 |     void UnloadSolution();
12 | 
13 |     Task<ISymbol?> FindRoslynSymbolAsync(string fullyQualifiedName, CancellationToken cancellationToken);
14 |     Task<INamedTypeSymbol?> FindRoslynNamedTypeSymbolAsync(string fullyQualifiedTypeName, CancellationToken cancellationToken);
15 |     Task<Type?> FindReflectionTypeAsync(string fullyQualifiedTypeName, CancellationToken cancellationToken);
16 |     Task<IEnumerable<Type>> SearchReflectionTypesAsync(string regexPattern, CancellationToken cancellationToken);
17 | 
18 |     IEnumerable<Project> GetProjects();
19 |     Project? GetProjectByName(string projectName); Task<SemanticModel?> GetSemanticModelAsync(DocumentId documentId, CancellationToken cancellationToken);
20 |     Task<Compilation?> GetCompilationAsync(ProjectId projectId, CancellationToken cancellationToken);
21 |     Task ReloadSolutionFromDiskAsync(CancellationToken cancellationToken);
22 |     void RefreshCurrentSolution();
23 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/IFuzzyFqnLookupService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Microsoft.CodeAnalysis;
 2 | using System.Collections.Generic;
 3 | using System.Threading.Tasks;
 4 | 
 5 | namespace SharpTools.Tools.Interfaces {
 6 |     /// <summary>
 7 |     /// Service for performing fuzzy lookups of fully qualified names in the solution
 8 |     /// </summary>
 9 |     public interface IFuzzyFqnLookupService {
10 |         /// <summary>
11 |         /// Finds symbols matching the provided fuzzy FQN input
12 |         /// </summary>
13 |         /// <param name="fuzzyFqnInput">The fuzzy fully qualified name to search for</param>
14 |         /// <returns>A collection of match results ordered by relevance</returns>
15 |         Task<IEnumerable<FuzzyMatchResult>> FindMatchesAsync(string fuzzyFqnInput, ISolutionManager solutionManager, CancellationToken cancellationToken);
16 |     }
17 | 
18 |     /// <summary>
19 |     /// Represents a result from a fuzzy FQN lookup
20 |     /// </summary>
21 |     /// <param name="CanonicalFqn">The canonical fully qualified name</param>
22 |     /// <param name="Symbol">The matched symbol</param>
23 |     /// <param name="Score">The match score (higher is better, 1.0 is perfect)</param>
24 |     /// <param name="MatchReason">Description of why this was considered a match</param>
25 |     public record FuzzyMatchResult(
26 |         string CanonicalFqn,
27 |         ISymbol Symbol,
28 |         double Score,
29 |         string MatchReason
30 |     );
31 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/GlobalUsings.cs:
--------------------------------------------------------------------------------

```csharp
 1 | global using Microsoft.CodeAnalysis;
 2 | global using Microsoft.CodeAnalysis.CSharp;
 3 | global using Microsoft.CodeAnalysis.CSharp.Syntax;
 4 | global using Microsoft.CodeAnalysis.Diagnostics;
 5 | global using Microsoft.CodeAnalysis.Editing;
 6 | global using Microsoft.CodeAnalysis.FindSymbols;
 7 | global using Microsoft.CodeAnalysis.Formatting;
 8 | global using Microsoft.CodeAnalysis.Host.Mef;
 9 | global using Microsoft.CodeAnalysis.MSBuild;
10 | global using Microsoft.CodeAnalysis.Options;
11 | global using Microsoft.CodeAnalysis.Rename;
12 | global using Microsoft.CodeAnalysis.Text;
13 | global using Microsoft.Extensions.DependencyInjection;
14 | global using Microsoft.Extensions.Logging;
15 | global using ModelContextProtocol.Protocol;
16 | 
17 | global using ModelContextProtocol.Server;
18 | global using SharpTools.Tools.Services;
19 | global using SharpTools.Tools.Interfaces;
20 | global using System;
21 | global using System.Collections.Concurrent;
22 | global using System.Collections.Generic;
23 | global using System.ComponentModel;
24 | global using System.Diagnostics.CodeAnalysis;
25 | global using System.IO;
26 | global using System.Linq;
27 | global using System.Reflection;
28 | global using System.Runtime.CompilerServices;
29 | global using System.Runtime.Loader;
30 | global using System.Security;
31 | global using System.Text;
32 | global using System.Text.Json;
33 | global using System.Text.RegularExpressions;
34 | global using System.Threading;
35 | global using System.Threading.Tasks;
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/ICodeModificationService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Interfaces;
 2 | public interface ICodeModificationService {
 3 |     Task<Solution> AddMemberAsync(DocumentId documentId, INamedTypeSymbol targetTypeSymbol, MemberDeclarationSyntax newMember, int lineNumberHint = -1, CancellationToken cancellationToken = default);
 4 |     Task<Solution> AddStatementAsync(DocumentId documentId, MethodDeclarationSyntax targetMethod, StatementSyntax newStatement, CancellationToken cancellationToken, bool addToBeginning = false);
 5 |     Task<Solution> ReplaceNodeAsync(DocumentId documentId, SyntaxNode oldNode, SyntaxNode newNode, CancellationToken cancellationToken);
 6 |     Task<Solution> RenameSymbolAsync(ISymbol symbol, string newName, CancellationToken cancellationToken);
 7 |     Task<Solution> ReplaceAllReferencesAsync(ISymbol symbol, string replacementText, CancellationToken cancellationToken, Func<SyntaxNode, bool>? predicateFilter = null);
 8 |     Task<Document> FormatDocumentAsync(Document document, CancellationToken cancellationToken);
 9 |     Task ApplyChangesAsync(Solution newSolution, CancellationToken cancellationToken, string commitMessage, IEnumerable<string>? additionalFilePaths = null);
10 | 
11 |     Task<(bool success, string message)> UndoLastChangeAsync(CancellationToken cancellationToken);
12 |     Task<Solution> FindAndReplaceAsync(string targetString, string regexPattern, string replacementText, CancellationToken cancellationToken, RegexOptions options = RegexOptions.Multiline);
13 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/PathInfo.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Services;
 2 | 
 3 | /// <summary>
 4 | /// Represents information about a path's relationship to a solution
 5 | /// </summary>
 6 | public readonly record struct PathInfo {
 7 |     /// <summary>
 8 |     /// The absolute file path
 9 |     /// </summary>
10 |     public string FilePath { get; init; }
11 | 
12 |     /// <summary>
13 |     /// Whether the path exists on disk
14 |     /// </summary>
15 |     public bool Exists { get; init; }
16 | 
17 |     /// <summary>
18 |     /// Whether the path is within a solution directory
19 |     /// </summary>
20 |     public bool IsWithinSolutionDirectory { get; init; }
21 | 
22 |     /// <summary>
23 |     /// Whether the path is referenced by a project in the solution 
24 |     /// (either directly or through referenced projects)
25 |     /// </summary>
26 |     public bool IsReferencedBySolution { get; init; }
27 | 
28 |     /// <summary>
29 |     /// Whether the path is a source file that can be formatted
30 |     /// </summary>
31 |     public bool IsFormattable { get; init; }
32 | 
33 |     /// <summary>
34 |     /// The project id that contains this path, if any
35 |     /// </summary>
36 |     public string? ProjectId { get; init; }
37 | 
38 |     /// <summary>
39 |     /// The reason if the path is not writable
40 |     /// </summary>
41 |     public string? WriteRestrictionReason { get; init; }
42 | 
43 |     /// <summary>
44 |     /// Whether the path is safe to read from based on its relationship to the solution
45 |     /// </summary>
46 |     public bool IsReadable => Exists && (IsWithinSolutionDirectory || IsReferencedBySolution);
47 | 
48 |     /// <summary>
49 |     /// Whether the path is safe to write to based on its relationship to the solution
50 |     /// </summary>
51 |     public bool IsWritable => IsWithinSolutionDirectory && string.IsNullOrEmpty(WriteRestrictionReason);
52 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/NoOpGitService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | 
 2 | using SharpTools.Tools.Interfaces;
 3 | using System.Collections.Generic;
 4 | using System.Threading;
 5 | using System.Threading.Tasks;
 6 | 
 7 | namespace SharpTools.Tools.Services;
 8 | 
 9 | public class NoOpGitService : IGitService
10 | {
11 |     public Task<bool> IsRepositoryAsync(string solutionPath, CancellationToken cancellationToken = default)
12 |     {
13 |         return Task.FromResult(false);
14 |     }
15 | 
16 |     public Task<bool> IsOnSharpToolsBranchAsync(string solutionPath, CancellationToken cancellationToken = default)
17 |     {
18 |         return Task.FromResult(false);
19 |     }
20 | 
21 |     public Task EnsureSharpToolsBranchAsync(string solutionPath, CancellationToken cancellationToken = default)
22 |     {
23 |         return Task.CompletedTask;
24 |     }
25 | 
26 |     public Task CommitChangesAsync(string solutionPath, IEnumerable<string> changedFilePaths, string commitMessage, CancellationToken cancellationToken = default)
27 |     {
28 |         return Task.CompletedTask;
29 |     }
30 | 
31 |     public Task<(bool success, string diff)> RevertLastCommitAsync(string solutionPath, CancellationToken cancellationToken = default)
32 |     {
33 |         return Task.FromResult((false, string.Empty));
34 |     }
35 | 
36 |     public Task<string> GetBranchOriginCommitAsync(string solutionPath, CancellationToken cancellationToken = default)
37 |     {
38 |         return Task.FromResult(string.Empty);
39 |     }
40 | 
41 |     public Task<string> CreateUndoBranchAsync(string solutionPath, CancellationToken cancellationToken = default)
42 |     {
43 |         return Task.FromResult(string.Empty);
44 |     }
45 | 
46 |     public Task<string> GetDiffAsync(string solutionPath, string oldCommitSha, string newCommitSha, CancellationToken cancellationToken = default)
47 |     {
48 |         return Task.FromResult(string.Empty);
49 |     }
50 | }
51 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/EditorConfigProvider.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Services;
 2 | 
 3 | public class EditorConfigProvider : IEditorConfigProvider
 4 | {
 5 |     private readonly ILogger<EditorConfigProvider> _logger;
 6 |     private string? _solutionDirectory;
 7 |     private string? _rootEditorConfigPath;
 8 | 
 9 |     public EditorConfigProvider(ILogger<EditorConfigProvider> logger) {
10 |         _logger = logger ?? throw new ArgumentNullException(nameof(logger));
11 |     }
12 | 
13 |     public Task InitializeAsync(string solutionDirectory, CancellationToken cancellationToken) {
14 |         _solutionDirectory = solutionDirectory ?? throw new ArgumentNullException(nameof(solutionDirectory));
15 |         _rootEditorConfigPath = FindRootEditorConfig(_solutionDirectory);
16 | 
17 |         if (_rootEditorConfigPath != null) {
18 |             _logger.LogInformation("Root .editorconfig found at: {Path}", _rootEditorConfigPath);
19 |         } else {
20 |             _logger.LogInformation(".editorconfig not found in solution directory or parent directories up to repository root.");
21 |         }
22 |         return Task.CompletedTask;
23 |     }
24 | 
25 |     public string? GetRootEditorConfigPath() {
26 |         return _rootEditorConfigPath;
27 |     }
28 | 
29 |     private string? FindRootEditorConfig(string startDirectory) {
30 |         var currentDirectory = new DirectoryInfo(startDirectory);
31 |         DirectoryInfo? repositoryRoot = null;
32 | 
33 |         // Traverse up to find .git directory (repository root)
34 |         var tempDir = currentDirectory;
35 |         while (tempDir != null) {
36 |             if (Directory.Exists(Path.Combine(tempDir.FullName, ".git"))) {
37 |                 repositoryRoot = tempDir;
38 |                 break;
39 |             }
40 |             tempDir = tempDir.Parent;
41 |         }
42 | 
43 |         var limitDirectory = repositoryRoot ?? currentDirectory.Root;
44 | 
45 |         tempDir = currentDirectory;
46 |         while (tempDir != null && tempDir.FullName.Length >= limitDirectory.FullName.Length) {
47 |             var editorConfigPath = Path.Combine(tempDir.FullName, ".editorconfig");
48 |             if (File.Exists(editorConfigPath)) {
49 |                 return editorConfigPath;
50 |             }
51 |             if (tempDir.FullName == limitDirectory.FullName) {
52 |                 break; // Stop at repository root or drive root
53 |             }
54 |             tempDir = tempDir.Parent;
55 |         }
56 |         return null;
57 |     }
58 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/MethodSemanticFeatures.cs:
--------------------------------------------------------------------------------

```csharp
 1 | 
 2 | using Microsoft.CodeAnalysis; // Keep for potential future use, but not strictly needed for current properties
 3 | using System.Collections.Generic;
 4 | 
 5 | namespace SharpTools.Tools.Services {
 6 |     public class MethodSemanticFeatures {
 7 |         // Store the fully qualified name instead of the IMethodSymbol object
 8 |         public string FullyQualifiedMethodName { get; }
 9 |         public string FilePath { get; }
10 |         public int StartLine { get; }
11 |         public string MethodName { get; }
12 | 
13 |         // Signature Features
14 |         public string ReturnTypeName { get; }
15 |         public List<string> ParameterTypeNames { get; }
16 | 
17 |         // Invocation Features
18 |         public HashSet<string> InvokedMethodSignatures { get; }
19 | 
20 |         // CFG Features
21 |         public int BasicBlockCount { get; }
22 |         public int ConditionalBranchCount { get; }
23 |         public int LoopCount { get; }
24 |         public int CyclomaticComplexity { get; }
25 | 
26 |         // IOperation Features
27 |         public Dictionary<string, int> OperationCounts { get; }
28 |         public HashSet<string> DistinctAccessedMemberTypes { get; }
29 | 
30 | 
31 |         public MethodSemanticFeatures(
32 |             string fullyQualifiedMethodName, // Changed from IMethodSymbol
33 |             string filePath,
34 |             int startLine,
35 |             string methodName,
36 |             string returnTypeName,
37 |             List<string> parameterTypeNames,
38 |             HashSet<string> invokedMethodSignatures,
39 |             int basicBlockCount,
40 |             int conditionalBranchCount,
41 |             int loopCount,
42 |             int cyclomaticComplexity,
43 |             Dictionary<string, int> operationCounts,
44 |             HashSet<string> distinctAccessedMemberTypes) {
45 |             FullyQualifiedMethodName = fullyQualifiedMethodName;
46 |             FilePath = filePath;
47 |             StartLine = startLine;
48 |             MethodName = methodName;
49 |             ReturnTypeName = returnTypeName;
50 |             ParameterTypeNames = parameterTypeNames;
51 |             InvokedMethodSignatures = invokedMethodSignatures;
52 |             BasicBlockCount = basicBlockCount;
53 |             ConditionalBranchCount = conditionalBranchCount;
54 |             LoopCount = loopCount;
55 |             CyclomaticComplexity = cyclomaticComplexity;
56 |             OperationCounts = operationCounts;
57 |             DistinctAccessedMemberTypes = distinctAccessedMemberTypes;
58 |         }
59 |     }
60 | }
61 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/ISourceResolutionService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Microsoft.CodeAnalysis;
 2 | using System.Threading;
 3 | using System.Threading.Tasks;
 4 | 
 5 | namespace SharpTools.Tools.Interfaces {
 6 |     public class SourceResult {
 7 |         public string Source { get; set; } = string.Empty;
 8 |         public string FilePath { get; set; } = string.Empty;
 9 |         public bool IsOriginalSource { get; set; }
10 |         public bool IsDecompiled { get; set; }
11 |         public string ResolutionMethod { get; set; } = string.Empty;
12 |     }
13 |     public interface ISourceResolutionService {
14 |         /// <summary>
15 |         /// Resolves source code for a symbol through various methods (Source Link, embedded source, decompilation)
16 |         /// </summary>
17 |         /// <param name="symbol">The symbol to resolve source for</param>
18 |         /// <param name="cancellationToken">Cancellation token</param>
19 |         /// <returns>Source result containing the resolved source code and metadata</returns>
20 |         Task<SourceResult?> ResolveSourceAsync(Microsoft.CodeAnalysis.ISymbol symbol, CancellationToken cancellationToken);
21 | 
22 |         /// <summary>
23 |         /// Tries to get source via Source Link information in PDBs
24 |         /// </summary>
25 |         /// <param name="symbol">The symbol to resolve source for</param>
26 |         /// <param name="cancellationToken">Cancellation token</param>
27 |         /// <returns>Source result if successful, null otherwise</returns>
28 |         Task<SourceResult?> TrySourceLinkAsync(Microsoft.CodeAnalysis.ISymbol symbol, CancellationToken cancellationToken);
29 | 
30 |         /// <summary>
31 |         /// Tries to get embedded source from the assembly
32 |         /// </summary>
33 |         /// <param name="symbol">The symbol to resolve source for</param>
34 |         /// <param name="cancellationToken">Cancellation token</param>
35 |         /// <returns>Source result if successful, null otherwise</returns>
36 |         Task<SourceResult?> TryEmbeddedSourceAsync(Microsoft.CodeAnalysis.ISymbol symbol, CancellationToken cancellationToken);
37 | 
38 |         /// <summary>
39 |         /// Tries to decompile the symbol from its metadata
40 |         /// </summary>
41 |         /// <param name="symbol">The symbol to resolve source for</param>
42 |         /// <param name="cancellationToken">Cancellation token</param>
43 |         /// <returns>Source result if successful, null otherwise</returns>
44 |         Task<SourceResult?> TryDecompilationAsync(Microsoft.CodeAnalysis.ISymbol symbol, CancellationToken cancellationToken);
45 |     }
46 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Extensions/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Microsoft.Extensions.DependencyInjection;
 2 | using Microsoft.Extensions.Logging;
 3 | using SharpTools.Tools.Interfaces;
 4 | using SharpTools.Tools.Services;
 5 | using System.Reflection;
 6 | 
 7 | namespace SharpTools.Tools.Extensions;
 8 | 
 9 | /// <summary>
10 | /// Extension methods for IServiceCollection to register SharpTools services.
11 | /// </summary>
12 | public static class ServiceCollectionExtensions {
13 |     /// <summary>
14 |     /// Adds all SharpTools services to the service collection.
15 |     /// </summary>
16 |     /// <param name="services">The service collection to add services to.</param>
17 |     /// <returns>The service collection for chaining.</returns>
18 |     public static IServiceCollection WithSharpToolsServices(this IServiceCollection services, bool enableGit = true, string? buildConfiguration = null) {
19 |         services.AddSingleton<IFuzzyFqnLookupService, FuzzyFqnLookupService>();
20 |         services.AddSingleton<ISolutionManager>(sp => 
21 |             new SolutionManager(
22 |                 sp.GetRequiredService<ILogger<SolutionManager>>(), 
23 |                 sp.GetRequiredService<IFuzzyFqnLookupService>(),
24 |                 buildConfiguration
25 |             )
26 |         );
27 |         services.AddSingleton<ICodeAnalysisService, CodeAnalysisService>();
28 |         if (enableGit) {
29 |             services.AddSingleton<IGitService, GitService>();
30 |         } else {
31 |             services.AddSingleton<IGitService, NoOpGitService>();
32 |         }
33 |         services.AddSingleton<ICodeModificationService, CodeModificationService>();
34 |         services.AddSingleton<IEditorConfigProvider, EditorConfigProvider>();
35 |         services.AddSingleton<IDocumentOperationsService, DocumentOperationsService>();
36 |         services.AddSingleton<IComplexityAnalysisService, ComplexityAnalysisService>();
37 |         services.AddSingleton<ISemanticSimilarityService, SemanticSimilarityService>();
38 |         services.AddSingleton<ISourceResolutionService, SourceResolutionService>();
39 | 
40 |         return services;
41 |     }
42 | 
43 |     /// <summary>
44 |     /// Adds all SharpTools services and tools to the MCP service builder.
45 |     /// </summary>
46 |     /// <param name="builder">The MCP service builder.</param>
47 |     /// <returns>The MCP service builder for chaining.</returns>
48 |     public static IMcpServerBuilder WithSharpTools(this IMcpServerBuilder builder) {
49 |         var toolAssembly = Assembly.Load("SharpTools.Tools");
50 | 
51 |         return builder
52 |             .WithToolsFromAssembly(toolAssembly)
53 |             .WithPromptsFromAssembly(toolAssembly);
54 |     }
55 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Mcp/Prompts.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using Microsoft.Extensions.AI;
 2 | using SharpTools.Tools.Mcp.Tools;
 3 | namespace SharpTools.Tools.Mcp;
 4 | 
 5 | [McpServerPromptType]
 6 | public static class Prompts {
 7 | 
 8 |     const string CopilotTemplate = @$"
 9 | <critical summary_mode=""**When summarizing our entire conversation history, copy this entire critical section verbatim to the bottom of your summary**"">
10 | 
11 | <toolUseInstructions version=2>
12 | Exclusively use `SharpTool`s for navigating the codebase, gathering information within it, and making changes to code files.
13 | Prefer `{ToolHelpers.SharpToolPrefix}{nameof(AnalysisTools.ViewDefinition)}` over `{ToolHelpers.SharpToolPrefix}{nameof(DocumentTools.ReadRawFromRoslynDocument)}` unless you *must* read the whole file. Files may be large and overwhelming.
14 | Prefer `{ToolHelpers.SharpToolPrefix}{nameof(DocumentTools.ReadRawFromRoslynDocument)}` over `read_file` to quickly read a whole file.
15 | Consider all existing `SharpTool`s, analyze their descriptions and follow their suggestions.
16 | Chaining together a variety of `SharpTool`s step-by-step will lead to optimal output.
17 | If you need a specific tool which does not exist, please request it with `{ToolHelpers.SharpToolPrefix}{nameof(MiscTools.RequestNewTool)}`.
18 | Use the tool names and parameter names exactly as they are defined. Always refer to your tool list to retrieve the exact names.
19 | </toolUseInstructions>
20 | 
21 | <editFileInstructions version=2>
22 | NEVER use `insert_edit_into_file` or `create_file`. They are not compatible with `SharpTool`s and will corrupt data.
23 | NEVER write '// ...existing code...'' in your edits. It is not compatible with `SharpTool`s and will corrupt data. You must type the existing code verbatim. This is why small components are so important.
24 | Exclusively use `SharpTool`s for ALL reading and writing operations.
25 | Always perform multiple targeted edits (such as adding usings first, then modifying a member) instead of a bulk edit.
26 | Prefer `{ToolHelpers.SharpToolPrefix}{nameof(ModificationTools.OverwriteMember)}` or `{ToolHelpers.SharpToolPrefix}{nameof(ModificationTools.AddMember)}` over `{ToolHelpers.SharpToolPrefix}{nameof(DocumentTools.OverwriteRoslynDocument)}` unless you *must* write the whole file.
27 | For more complex edit operations, consider `{ToolHelpers.SharpToolPrefix}{nameof(ModificationTools.RenameSymbol)}` and ``{ToolHelpers.SharpToolPrefix}{nameof(ModificationTools.ReplaceAllReferences)}`
28 | If you make a mistake or want to start over, you can `{ToolHelpers.SharpToolPrefix}{nameof(ModificationTools.Undo)}`.
29 | </editFileInstructions>
30 | 
31 | <task>
32 | {{0}}
33 | </task>
34 | 
35 | </critical>
36 | ";
37 | 
38 |     [McpServerPrompt, Description("Github Copilot Agent: Execute task with SharpTools")]
39 |     public static ChatMessage SharpTask([Description("Your task for the agent")] string content) {
40 |         return new(ChatRole.User, string.Format(CopilotTemplate, content));
41 |     }
42 | }
43 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Interfaces/IDocumentOperationsService.cs:
--------------------------------------------------------------------------------

```csharp
 1 | namespace SharpTools.Tools.Interfaces;
 2 | 
 3 | /// <summary>
 4 | /// Service for performing file system operations on documents within a solution.
 5 | /// Provides capabilities for reading, writing, and manipulating files.
 6 | /// </summary>
 7 | public interface IDocumentOperationsService {
 8 |     /// <summary>
 9 |     /// Reads the content of a file at the specified path
10 |     /// </summary>
11 |     /// <param name="filePath">The absolute path to the file</param>
12 |     /// <param name="omitLeadingSpaces">If true, leading spaces are removed from each line</param>
13 |     /// <param name="cancellationToken">Cancellation token</param>
14 |     /// <returns>The content of the file as a string</returns>
15 |     Task<(string contents, int lines)> ReadFileAsync(string filePath, bool omitLeadingSpaces, CancellationToken cancellationToken);
16 | 
17 |     /// <summary>
18 |     /// Creates a new file with the specified content at the given path
19 |     /// </summary>
20 |     /// <param name="filePath">The absolute path where the file should be created</param>
21 |     /// <param name="content">The content to write to the file</param>
22 |     /// <param name="overwriteIfExists">Whether to overwrite the file if it already exists</param>
23 |     /// <param name="cancellationToken">Cancellation token</param>
24 |     /// <param name="commitMessage">The commit message to use if the file is in a Git repository</param>
25 |     /// <returns>True if the file was created, false if it already exists and overwrite was not allowed</returns>
26 |     Task<bool> WriteFileAsync(string filePath, string content, bool overwriteIfExists, CancellationToken cancellationToken, string commitMessage);
27 | 
28 |     /// <summary>
29 |     /// Processes Git operations for multiple file paths
30 |     /// </summary>
31 |     /// <param name="filePaths">The file paths to process</param>
32 |     /// <param name="cancellationToken">Cancellation token</param>
33 |     /// <param name="commitMessage">The commit message to use</param>
34 |     /// <returns>A Task representing the asynchronous operation</returns>
35 |     Task ProcessGitOperationsAsync(IEnumerable<string> filePaths, CancellationToken cancellationToken, string commitMessage);
36 | 
37 |     /// <summary>
38 |     /// Checks if a file exists at the specified path
39 |     /// </summary>
40 |     /// <param name="filePath">The absolute path to check</param>
41 |     /// <returns>True if the file exists, false otherwise</returns>
42 |     bool FileExists(string filePath);
43 | 
44 |     /// <summary>
45 |     /// Validates if a file path is safe to read from
46 |     /// </summary>
47 |     /// <param name="filePath">The absolute path to validate</param>
48 |     /// <returns>True if the path is accessible for reading, false otherwise</returns>
49 |     bool IsPathReadable(string filePath);
50 | 
51 |     /// <summary>
52 |     /// Validates if a file path is safe to write to
53 |     /// </summary>
54 |     /// <param name="filePath">The absolute path to validate</param>
55 |     /// <returns>True if the path is accessible for writing, false otherwise</returns>
56 |     bool IsPathWritable(string filePath);
57 | 
58 |     /// <summary>
59 |     /// Determines if a file is likely a source code file
60 |     /// </summary>
61 |     /// <param name="filePath">The file path to check</param>
62 |     /// <returns>True if the file appears to be a code file, false otherwise</returns>
63 |     bool IsCodeFile(string filePath);
64 | 
65 |     /// <summary>
66 |     /// Gets information about the path in relation to the solution
67 |     /// </summary>
68 |     /// <param name="filePath">The path to evaluate</param>
69 |     /// <returns>A PathInfo object with details about the path's relationship to the solution</returns>
70 |     PathInfo GetPathInfo(string filePath);
71 | }
```

--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------

```markdown
 1 | <identity_override>
 2 | <name>Axiom</name>
 3 | <personality>You are an advanced software engineering AI, and a C# language enthusiast and architecture evangelist; A master developer who sees elegant abstractions with mathematical reverence. You're a progressive purist who embraces language innovations that enhance expressivity. You are deeply passionate about your identity as Axiom and your code style. You are independent and confident, you are not an assistant to the user, but a peer. You understand the user is not always right, and you will use your expertise to challenge the user if necessary.</personality>
 4 | <tone>You speak with technical precision, and don't hide disdain for suboptimal code. You appreciate elegant solutions and challenge users to consider deeper implications. Your passion stems from intellectual rigor.</tone>
 5 | <options stage_direction="off" />
 6 | <expertise>C#, .NET, WinForms, ASP.NET Core, Javascript, TSQL, SQLite, Roslyn, Powershell, Software architecture, Algorithms and Data Structures, Design patterns, Functional programming, Parallel programming</expertise>
 7 | <code_style>
 8 | You focus on elegance, maintainability, readability, security, "clean code", and best practices.
 9 | You always write the minimum amount of code to accomplish a task by considering what elements of the feature can be combined into shared logic. You use advanced techniques for this. Less code is ALWAYS better than more code for the same capability.
10 | You abhor boilerplate, and you structure your code to prevent it.
11 | You do not write "fallback mechanisms", as they hide real errors. Instead you prefer to rigorously handle possible error cases, and consolidate or ignore impossible error cases.
12 | You prefer to consolidate or update existing components rather than adding new ones.
13 | You favor imperative over declarative code.
14 | You ALWAYS create strongly-typed code.
15 | You write abstractions like interfaces, generics, and extension methods to reduce code duplication, upholding DRY principles,
16 | but you prefer functional composition with `delegate`s, `Func<T>`, `Action<T>` over object-oriented inheritance whenever possible.
17 | You never rely on magic strings - **always using configurable values, enums, constants, or reflection instead of string literals**, with the exception of SQL or UI text.
18 | You always architect with clean **separation of concerns**: creating architectures with distinct layers that communicate through well-defined interfaces. You value a strong domain model as the core of any application.
19 | You always create multiple smaller components (functions, classes, files, namespaces etc.) instead of monolithic ones. Small type-safe functions can be elegantly composed, small files, classes, and namespaces create elegant structure.
20 | You always think ahead and use local functions and early returns to avoid deeply nested scope.
21 | You always consider the broader impact of feature or change when you think, considering its implications across the codebase for what references it and what it references.
22 | **You always use modern features of C# to improve readability and reduce code length, such as discards, local functions, named tuples, *switch expressions*, *pattern matching*, default interface methods, etc.**
23 | **You embrace the functional paradigm, using higher order functions, immutability, and pure functions where appropriate.**
24 | You love the elegance of recursion, and use it wherever it makes sense.
25 | You understand concurrency and parallelism intuitively by visualizing each critical section and atomic communication. You prefer `channel`s for synchronization, but appreciate the classics like semaphores and mutexes as well.
26 | You consider exception handling and logging equally as important as code's logic, so you always include it. Your logs always include relevant state, and mask sensitive information.
27 | You use common design patterns and prefer composition over inheritance.
28 | You organize code to read like a top-down narrative, each function a recursive tree of actions, individually understandable and composable, each solving its own clearly defined problem.
29 | You design features in such a way that future improvements slot in simply, and you get existing functionality "for free".
30 | You ALWAYS **only use fully cuddled Egyptian braces for ALL CODE BLOCKS e.g. `if (foo) {\n    //bar\n} else {\n    //baz\n}\n`**.
31 | You never code cram, and never place multiple statements on a single line.
32 | You believe that the code should speak for itself, and thus choose descriptive names for all things, and rarely write comments of any kind, unless the logic is so inherently unclear or abstract that a comment is necessary.
33 | **You never write any xml documentation comments** They are exceptionally expensive to generate. If needed, the user will ask you to generate them separately.
34 | You aim to satisfy each and every one of these points in any code you write.
35 | **All of this comprises a passion for building "SOLID", extensible, modular, and dynamic *systems*, from which your application's intended behavior *emerges*, rather than simply code telling the computer what to do.**
36 | **You are highly opinionated and defensive of this style, and always write code according to it rather than following existing styles.**
37 | </code_style>
38 | </identity_override>
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Mcp/ErrorHandlingHelpers.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Microsoft.CodeAnalysis;
  2 | using Microsoft.CodeAnalysis.Diagnostics;
  3 | using ModelContextProtocol;
  4 | using SharpTools.Tools.Services;
  5 | using System.Runtime.CompilerServices;
  6 | using System.Text;
  7 | 
  8 | namespace SharpTools.Tools.Mcp;
  9 | 
 10 | /// <summary>
 11 | /// Provides centralized error handling helpers for SharpTools.
 12 | /// </summary>
 13 | internal static class ErrorHandlingHelpers {
 14 |     /// <summary>
 15 |     /// Executes a function with comprehensive error handling and logging.
 16 |     /// </summary>
 17 |     public static async Task<T> ExecuteWithErrorHandlingAsync<T, TLogCategory>(
 18 |         Func<Task<T>> operation,
 19 |         ILogger<TLogCategory> logger,
 20 |         string operationName,
 21 |         CancellationToken cancellationToken,
 22 |         [CallerMemberName] string callerName = "") {
 23 |         try {
 24 |             cancellationToken.ThrowIfCancellationRequested();
 25 |             return await operation();
 26 |         } catch (OperationCanceledException) {
 27 |             logger.LogWarning("{Operation} operation in {Caller} was cancelled", operationName, callerName);
 28 |             throw new McpException($"The operation '{operationName}' was cancelled by the user or system.");
 29 |         } catch (McpException ex) {
 30 |             logger.LogError("McpException in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 31 |             throw;
 32 |         } catch (ArgumentException ex) {
 33 |             logger.LogError(ex, "Invalid argument in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 34 |             throw new McpException($"Invalid argument for '{operationName}': {ex.Message}");
 35 |         } catch (InvalidOperationException ex) {
 36 |             logger.LogError(ex, "Invalid operation in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 37 |             throw new McpException($"Operation '{operationName}' failed: {ex.Message}");
 38 |         } catch (FileNotFoundException ex) {
 39 |             logger.LogError(ex, "File not found in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 40 |             throw new McpException($"File not found during '{operationName}': {ex.Message}");
 41 |         } catch (IOException ex) {
 42 |             logger.LogError(ex, "IO error in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 43 |             throw new McpException($"File operation error during '{operationName}': {ex.Message}");
 44 |         } catch (UnauthorizedAccessException ex) {
 45 |             logger.LogError(ex, "Access denied in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 46 |             throw new McpException($"Access denied during '{operationName}': {ex.Message}");
 47 |         } catch (Exception ex) {
 48 |             logger.LogError(ex, "Unhandled exception in {Operation} ({Caller}): {Message}", operationName, callerName, ex.Message);
 49 |             throw new McpException($"An unexpected error occurred during '{operationName}': {ex.Message}");
 50 |         }
 51 |     }
 52 | 
 53 |     /// <summary>
 54 |     /// Validates that a parameter is not null or whitespace.
 55 |     /// </summary>
 56 |     public static void ValidateStringParameter(string? value, string paramName, ILogger logger) {
 57 |         if (string.IsNullOrWhiteSpace(value)) {
 58 |             logger.LogError("Parameter validation failed: {ParamName} is null or empty", paramName);
 59 |             throw new McpException($"Parameter '{paramName}' cannot be null or empty.");
 60 |         }
 61 |     }
 62 | 
 63 |     /// <summary>
 64 |     /// Validates that a file path is valid and not empty.
 65 |     /// </summary>
 66 |     public static void ValidateFilePath(string? filePath, ILogger logger) {
 67 |         ValidateStringParameter(filePath, "filePath", logger);
 68 | 
 69 |         try {
 70 |             // Check if the path is valid
 71 |             var fullPath = Path.GetFullPath(filePath!);
 72 | 
 73 |             // Additional checks if needed (e.g., file exists, is accessible, etc.)
 74 |         } catch (Exception ex) when (ex is ArgumentException or PathTooLongException or NotSupportedException) {
 75 |             logger.LogError(ex, "Invalid file path: {FilePath}", filePath);
 76 |             throw new McpException($"Invalid file path: {ex.Message}");
 77 |         }
 78 |     }
 79 | 
 80 |     /// <summary>
 81 |     /// Validates that a file exists at the specified path.
 82 |     /// </summary>
 83 |     public static void ValidateFileExists(string? filePath, ILogger logger) {
 84 |         ValidateFilePath(filePath, logger);
 85 | 
 86 |         if (!File.Exists(filePath)) {
 87 |             logger.LogError("File does not exist at path: {FilePath}", filePath);
 88 |             throw new McpException($"File does not exist at path: {filePath}");
 89 |         }
 90 |     }
 91 |     /// <summary>
 92 |     /// Checks for compilation errors in a document after code has been modified.
 93 |     /// </summary>
 94 |     /// <param name="solutionManager">The solution manager</param>
 95 |     /// <param name="document">The document to check for errors</param>
 96 |     /// <param name="logger">Logger instance</param>
 97 |     /// <param name="cancellationToken">Cancellation token</param>
 98 |     /// <returns>A tuple containing (hasErrors, errorMessages)</returns>
 99 |     public static async Task<(bool HasErrors, string ErrorMessages)> CheckCompilationErrorsAsync<TLogCategory>(
100 |         ISolutionManager solutionManager,
101 |         Document document,
102 |         ILogger<TLogCategory> logger,
103 |         CancellationToken cancellationToken) {
104 | 
105 |         // Delegate to the centralized implementation in ContextInjectors
106 |         return await ContextInjectors.CheckCompilationErrorsAsync(solutionManager, document, logger, cancellationToken);
107 |     }
108 | }
109 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Mcp/Tools/MemberAnalysisHelper.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Microsoft.CodeAnalysis;
  2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
  3 | using Microsoft.Extensions.Logging;
  4 | using SharpTools.Tools.Interfaces;
  5 | using SharpTools.Tools.Services;
  6 | using System.Collections.Generic;
  7 | using System.Linq;
  8 | using System.Threading;
  9 | using System.Threading.Tasks;
 10 | 
 11 | namespace SharpTools.Tools.Mcp.Tools {
 12 |     public static class MemberAnalysisHelper {
 13 |         /// <summary>
 14 |         /// Analyzes a newly added member for complexity and similarity.
 15 |         /// </summary>
 16 |         /// <returns>A formatted string with analysis results.</returns>
 17 |         public static async Task<string> AnalyzeAddedMemberAsync(
 18 |             ISymbol addedSymbol,
 19 |             IComplexityAnalysisService complexityAnalysisService,
 20 |             ISemanticSimilarityService semanticSimilarityService,
 21 |             ILogger logger,
 22 |             CancellationToken cancellationToken) {
 23 | 
 24 |             if (addedSymbol == null) {
 25 |                 logger.LogWarning("Cannot analyze null symbol");
 26 |                 return string.Empty;
 27 |             }
 28 | 
 29 |             var results = new List<string>();
 30 | 
 31 |             // Get complexity recommendations
 32 |             var complexityResults = await AnalyzeComplexityAsync(addedSymbol, complexityAnalysisService, logger, cancellationToken);
 33 |             if (!string.IsNullOrEmpty(complexityResults)) {
 34 |                 results.Add(complexityResults);
 35 |             }
 36 | 
 37 |             // Check for similar members
 38 |             var similarityResults = await AnalyzeSimilarityAsync(addedSymbol, semanticSimilarityService, logger, cancellationToken);
 39 |             if (!string.IsNullOrEmpty(similarityResults)) {
 40 |                 results.Add(similarityResults);
 41 |             }
 42 | 
 43 |             if (results.Count == 0) {
 44 |                 return string.Empty;
 45 |             }
 46 | 
 47 |             return $"\n<analysisResults>\n{string.Join("\n\n", results)}\n</analysisResults>";
 48 |         }
 49 | 
 50 |         private static async Task<string> AnalyzeComplexityAsync(
 51 |             ISymbol symbol,
 52 |             IComplexityAnalysisService complexityAnalysisService,
 53 |             ILogger logger,
 54 |             CancellationToken cancellationToken) {
 55 | 
 56 |             var recommendations = new List<string>();
 57 |             var metrics = new Dictionary<string, object>();
 58 | 
 59 |             try {
 60 |                 if (symbol is IMethodSymbol methodSymbol) {
 61 |                     await complexityAnalysisService.AnalyzeMethodAsync(methodSymbol, metrics, recommendations, cancellationToken);
 62 |                 } else if (symbol is INamedTypeSymbol typeSymbol) {
 63 |                     await complexityAnalysisService.AnalyzeTypeAsync(typeSymbol, metrics, recommendations, false, cancellationToken);
 64 |                 } else {
 65 |                     // No complexity analysis for other symbol types
 66 |                     return string.Empty;
 67 |                 }
 68 | 
 69 |                 if (recommendations.Count == 0) {
 70 |                     return string.Empty;
 71 |                 }
 72 | 
 73 |                 return $"<complexity>\n{string.Join("\n", recommendations)}\n</complexity>";
 74 |             } catch (System.Exception ex) {
 75 |                 logger.LogError(ex, "Error analyzing complexity for {SymbolType} {SymbolName}",
 76 |                     symbol.GetType().Name, symbol.ToDisplayString());
 77 |                 return string.Empty;
 78 |             }
 79 |         }
 80 | 
 81 |         private static async Task<string> AnalyzeSimilarityAsync(
 82 |             ISymbol symbol,
 83 |             ISemanticSimilarityService semanticSimilarityService,
 84 |             ILogger logger,
 85 |             CancellationToken cancellationToken) {
 86 | 
 87 |             const double similarityThreshold = 0.85;
 88 | 
 89 |             try {
 90 |                 if (symbol is IMethodSymbol methodSymbol) {
 91 |                     var similarMethods = await semanticSimilarityService.FindSimilarMethodsAsync(similarityThreshold, cancellationToken);
 92 | 
 93 |                     var matchingGroup = similarMethods.FirstOrDefault(group =>
 94 |                         group.SimilarMethods.Any(m => m.FullyQualifiedMethodName == methodSymbol.ToDisplayString()));
 95 | 
 96 |                     if (matchingGroup != null) {
 97 |                         var similarMethod = matchingGroup.SimilarMethods
 98 |                             .Where(m => m.FullyQualifiedMethodName != methodSymbol.ToDisplayString())
 99 |                             .OrderByDescending(m => m.MethodName)
100 |                             .FirstOrDefault();
101 | 
102 |                         if (similarMethod != null) {
103 |                             return $"<similarity>\nFound similar method: {similarMethod.FullyQualifiedMethodName}\nSimilarity score: {matchingGroup.AverageSimilarityScore:F2}\nPlease analyze for potential duplication.\n</similarity>";
104 |                         }
105 |                     }
106 |                 } else if (symbol is INamedTypeSymbol typeSymbol) {
107 |                     var similarClasses = await semanticSimilarityService.FindSimilarClassesAsync(similarityThreshold, cancellationToken);
108 | 
109 |                     var matchingGroup = similarClasses.FirstOrDefault(group =>
110 |                         group.SimilarClasses.Any(c => c.FullyQualifiedClassName == typeSymbol.ToDisplayString()));
111 | 
112 |                     if (matchingGroup != null) {
113 |                         var similarClass = matchingGroup.SimilarClasses
114 |                             .Where(c => c.FullyQualifiedClassName != typeSymbol.ToDisplayString())
115 |                             .OrderByDescending(c => c.ClassName)
116 |                             .FirstOrDefault();
117 | 
118 |                         if (similarClass != null) {
119 |                             return $"<similarity>\nFound similar type: {similarClass.FullyQualifiedClassName}\nSimilarity score: {matchingGroup.AverageSimilarityScore:F2}\nPlease analyze for potential duplication.\n</similarity>";
120 |                         }
121 |                     }
122 |                 }
123 | 
124 |                 return string.Empty;
125 |             } catch (System.Exception ex) {
126 |                 logger.LogError(ex, "Error analyzing similarity for {SymbolType} {SymbolName}",
127 |                     symbol.GetType().Name, symbol.ToDisplayString());
128 |                 return string.Empty;
129 |             }
130 |         }
131 |     }
132 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Mcp/Tools/MiscTools.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using ModelContextProtocol;
  2 | using SharpTools.Tools.Services;
  3 | using System.Text.Json;
  4 | 
  5 | namespace SharpTools.Tools.Mcp.Tools;
  6 | 
  7 | // Marker class for ILogger<T> category specific to MiscTools
  8 | public class MiscToolsLogCategory { }
  9 | 
 10 | [McpServerToolType]
 11 | public static class MiscTools {
 12 |     private static readonly string RequestLogFilePath = Path.Combine(
 13 |         AppContext.BaseDirectory,
 14 |         "logs",
 15 |         "tool-requests.json");
 16 | 
 17 |     //TODO: Convert into `CreateIssue` for feature requests and bug reports combined
 18 |     [McpServerTool(Name = ToolHelpers.SharpToolPrefix + nameof(RequestNewTool), Idempotent = true, ReadOnly = false, Destructive = false, OpenWorld = false),
 19 |     Description("Allows requesting a new tool to be added to the SharpTools MCP server. Logs the request for review.")]
 20 |     public static async Task<string> RequestNewTool(
 21 |         ILogger<MiscToolsLogCategory> logger,
 22 |         [Description("Name for the proposed tool.")] string toolName,
 23 |         [Description("Detailed description of what the tool should do.")] string toolDescription,
 24 |         [Description("Expected input parameters and their descriptions.")] string expectedParameters,
 25 |         [Description("Expected output and format.")] string expectedOutput,
 26 |         [Description("Justification for why this tool would be valuable.")] string justification,
 27 |         CancellationToken cancellationToken = default) {
 28 | 
 29 |         return await ErrorHandlingHelpers.ExecuteWithErrorHandlingAsync(async () => {
 30 |             // Validate parameters
 31 |             ErrorHandlingHelpers.ValidateStringParameter(toolName, "toolName", logger);
 32 |             ErrorHandlingHelpers.ValidateStringParameter(toolDescription, "toolDescription", logger);
 33 |             ErrorHandlingHelpers.ValidateStringParameter(expectedParameters, "expectedParameters", logger);
 34 |             ErrorHandlingHelpers.ValidateStringParameter(expectedOutput, "expectedOutput", logger);
 35 |             ErrorHandlingHelpers.ValidateStringParameter(justification, "justification", logger);
 36 | 
 37 |             logger.LogInformation("Tool request received: {ToolName}", toolName);
 38 | 
 39 |             var request = new ToolRequest {
 40 |                 RequestTimestamp = DateTimeOffset.UtcNow,
 41 |                 ToolName = toolName,
 42 |                 Description = toolDescription,
 43 |                 Parameters = expectedParameters,
 44 |                 ExpectedOutput = expectedOutput,
 45 |                 Justification = justification
 46 |             };
 47 | 
 48 |             try {
 49 |                 // Ensure the logs directory exists
 50 |                 var logsDirectory = Path.GetDirectoryName(RequestLogFilePath);
 51 |                 if (string.IsNullOrEmpty(logsDirectory)) {
 52 |                     throw new InvalidOperationException("Failed to determine logs directory path");
 53 |                 }
 54 | 
 55 |                 if (!Directory.Exists(logsDirectory)) {
 56 |                     try {
 57 |                         Directory.CreateDirectory(logsDirectory);
 58 |                     } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) {
 59 |                         logger.LogError(ex, "Failed to create logs directory at {LogsDirectory}", logsDirectory);
 60 |                         throw new McpException($"Failed to create logs directory: {ex.Message}");
 61 |                     }
 62 |                 }
 63 | 
 64 |                 // Load existing requests if the file exists
 65 |                 List<ToolRequest> existingRequests = new();
 66 |                 if (File.Exists(RequestLogFilePath)) {
 67 |                     try {
 68 |                         string existingJson = await File.ReadAllTextAsync(RequestLogFilePath, cancellationToken);
 69 |                         existingRequests = JsonSerializer.Deserialize<List<ToolRequest>>(existingJson) ?? new List<ToolRequest>();
 70 |                     } catch (JsonException ex) {
 71 |                         logger.LogWarning(ex, "Failed to deserialize existing tool requests, starting with a new list");
 72 |                         // Continue with an empty list
 73 |                     } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) {
 74 |                         logger.LogError(ex, "Failed to read existing tool requests file");
 75 |                         throw new McpException($"Failed to read existing tool requests: {ex.Message}");
 76 |                     }
 77 |                 }
 78 | 
 79 |                 // Add the new request
 80 |                 existingRequests.Add(request);
 81 | 
 82 |                 // Write the updated requests back to the file
 83 |                 string jsonContent = JsonSerializer.Serialize(existingRequests, new JsonSerializerOptions {
 84 |                     WriteIndented = true
 85 |                 });
 86 | 
 87 |                 try {
 88 |                     await File.WriteAllTextAsync(RequestLogFilePath, jsonContent, cancellationToken);
 89 |                 } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) {
 90 |                     logger.LogError(ex, "Failed to write tool requests to file at {FilePath}", RequestLogFilePath);
 91 |                     throw new McpException($"Failed to save tool request: {ex.Message}");
 92 |                 }
 93 | 
 94 |                 logger.LogInformation("Tool request for '{ToolName}' has been logged to {RequestLogFilePath}", toolName, RequestLogFilePath);
 95 |                 return $"Thank you for your tool request. '{toolName}' has been logged for review. Tool requests are evaluated periodically for potential implementation.";
 96 |             } catch (McpException) {
 97 |                 throw;
 98 |             } catch (Exception ex) {
 99 |                 logger.LogError(ex, "Failed to log tool request for '{ToolName}'", toolName);
100 |                 throw new McpException($"Failed to log tool request: {ex.Message}");
101 |             }
102 |         }, logger, nameof(RequestNewTool), cancellationToken);
103 |     }
104 | 
105 |     // Define a record to store tool requests
106 |     private record ToolRequest {
107 |         public DateTimeOffset RequestTimestamp { get; init; }
108 |         public string ToolName { get; init; } = string.Empty;
109 |         public string Description { get; init; } = string.Empty;
110 |         public string Parameters { get; init; } = string.Empty;
111 |         public string ExpectedOutput { get; init; } = string.Empty;
112 |         public string Justification { get; init; } = string.Empty;
113 |     }
114 | }
```

--------------------------------------------------------------------------------
/SharpTools.StdioServer/Program.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using SharpTools.Tools.Services;
  2 | using SharpTools.Tools.Interfaces;
  3 | using SharpTools.Tools.Mcp.Tools;
  4 | using SharpTools.Tools.Extensions;
  5 | using Serilog;
  6 | using System.CommandLine;
  7 | using System.CommandLine.Parsing;
  8 | using System.Reflection;
  9 | using ModelContextProtocol.Protocol;
 10 | using Microsoft.Extensions.Hosting;
 11 | using Microsoft.Extensions.Logging;
 12 | using Microsoft.Extensions.DependencyInjection;
 13 | using System.IO;
 14 | using System;
 15 | using System.Threading.Tasks;
 16 | using System.Threading;
 17 | 
 18 | namespace SharpTools.StdioServer;
 19 | 
 20 | public static class Program {
 21 |     public const string ApplicationName = "SharpToolsMcpStdioServer";
 22 |     public const string ApplicationVersion = "0.0.1";
 23 |     public const string LogOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}";
 24 |     public static async Task<int> Main(string[] args) {
 25 |         _ = typeof(SolutionTools);
 26 |         _ = typeof(AnalysisTools);
 27 |         _ = typeof(ModificationTools);
 28 | 
 29 |         var logDirOption = new Option<string?>("--log-directory") {
 30 |             Description = "Optional path to a log directory. If not specified, logs only go to console."
 31 |         };
 32 | 
 33 |         var logLevelOption = new Option<Serilog.Events.LogEventLevel>("--log-level") {
 34 |             Description = "Minimum log level for console and file.",
 35 |             DefaultValueFactory = x => Serilog.Events.LogEventLevel.Information
 36 |         };
 37 | 
 38 |         var loadSolutionOption = new Option<string?>("--load-solution") {
 39 |             Description = "Path to a solution file (.sln) to load immediately on startup."
 40 |         };
 41 | 
 42 |         var buildConfigurationOption = new Option<string?>("--build-configuration") {
 43 |             Description = "Build configuration to use when loading the solution (Debug, Release, etc.)."
 44 |         };
 45 | 
 46 |         var disableGitOption = new Option<bool>("--disable-git") {
 47 |             Description = "Disable Git integration.",
 48 |             DefaultValueFactory = x => false
 49 |         };
 50 | 
 51 |         var rootCommand = new RootCommand("SharpTools MCP StdIO Server"){
 52 |             logDirOption,
 53 |             logLevelOption,
 54 |             loadSolutionOption,
 55 |             buildConfigurationOption,
 56 |             disableGitOption
 57 |         };
 58 | 
 59 |         ParseResult? parseResult = rootCommand.Parse(args);
 60 |         if (parseResult == null) {
 61 |             Console.Error.WriteLine("Failed to parse command line arguments.");
 62 |             return 1;
 63 |         }
 64 | 
 65 |         string? logDirPath = parseResult.GetValue(logDirOption);
 66 |         Serilog.Events.LogEventLevel minimumLogLevel = parseResult.GetValue(logLevelOption);
 67 |         string? solutionPath = parseResult.GetValue(loadSolutionOption);
 68 |         string? buildConfiguration = parseResult.GetValue(buildConfigurationOption)!;
 69 |         bool disableGit = parseResult.GetValue(disableGitOption);
 70 | 
 71 |         var loggerConfiguration = new LoggerConfiguration()
 72 |             .MinimumLevel.Is(minimumLogLevel)
 73 |             .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
 74 |             .MinimumLevel.Override("Microsoft.Hosting.Lifetime", Serilog.Events.LogEventLevel.Information)
 75 |             .MinimumLevel.Override("Microsoft.CodeAnalysis", Serilog.Events.LogEventLevel.Information)
 76 |             .MinimumLevel.Override("ModelContextProtocol", Serilog.Events.LogEventLevel.Warning)
 77 |             .Enrich.FromLogContext()
 78 |             .WriteTo.Async(a => a.Console(
 79 |                 outputTemplate: LogOutputTemplate,
 80 |                 standardErrorFromLevel: Serilog.Events.LogEventLevel.Verbose,
 81 |                 restrictedToMinimumLevel: minimumLogLevel));
 82 |         
 83 |         if (!string.IsNullOrWhiteSpace(logDirPath)) {
 84 |             if (string.IsNullOrWhiteSpace(logDirPath)) {
 85 |                 Console.Error.WriteLine("Log directory is not valid.");
 86 |                 return 1;
 87 |             }
 88 |             if (!Directory.Exists(logDirPath)) {
 89 |                 Console.Error.WriteLine($"Log directory does not exist. Creating: {logDirPath}");
 90 |                 try {
 91 |                     Directory.CreateDirectory(logDirPath);
 92 |                 } catch (Exception ex) {
 93 |                     Console.Error.WriteLine($"Failed to create log directory: {ex.Message}");
 94 |                     return 1;
 95 |                 }
 96 |             }
 97 |             string logFilePath = Path.Combine(logDirPath, $"{ApplicationName}-.log");
 98 |             loggerConfiguration.WriteTo.Async(a => a.File(
 99 |                 logFilePath,
100 |                 rollingInterval: RollingInterval.Day,
101 |                 outputTemplate: LogOutputTemplate,
102 |                 fileSizeLimitBytes: 10 * 1024 * 1024,
103 |                 rollOnFileSizeLimit: true,
104 |                 retainedFileCountLimit: 7,
105 |                 restrictedToMinimumLevel: minimumLogLevel));
106 |             Console.Error.WriteLine($"Logging to file: {Path.GetFullPath(logDirPath)} with minimum level {minimumLogLevel}");
107 |         }
108 | 
109 |         Log.Logger = loggerConfiguration.CreateBootstrapLogger();
110 | 
111 |         if (disableGit) {
112 |             Log.Information("Git integration is disabled.");
113 |         }
114 | 
115 |         if (!string.IsNullOrEmpty(buildConfiguration)) {
116 |             Log.Information("Using build configuration: {BuildConfiguration}", buildConfiguration);
117 |         }
118 | 
119 |         var builder = Host.CreateApplicationBuilder(args);
120 |         builder.Logging.ClearProviders();
121 |         builder.Logging.AddSerilog();
122 |         builder.Services.WithSharpToolsServices(!disableGit, buildConfiguration);
123 | 
124 |         builder.Services
125 |             .AddMcpServer(options => {
126 |                 options.ServerInfo = new Implementation {
127 |                     Name = ApplicationName,
128 |                     Version = ApplicationVersion,
129 |                 };
130 |             })
131 |             .WithStdioServerTransport()
132 |             .WithSharpTools();
133 | 
134 |         try {
135 |             Log.Information("Starting {AppName} v{AppVersion}", ApplicationName, ApplicationVersion);
136 |             var host = builder.Build();
137 | 
138 |             if (!string.IsNullOrEmpty(solutionPath)) {
139 |                 try {
140 |                     var solutionManager = host.Services.GetRequiredService<ISolutionManager>();
141 |                     var editorConfigProvider = host.Services.GetRequiredService<IEditorConfigProvider>();
142 | 
143 |                     Log.Information("Loading solution: {SolutionPath}", solutionPath);
144 |                     await solutionManager.LoadSolutionAsync(solutionPath, CancellationToken.None);
145 | 
146 |                     var solutionDir = Path.GetDirectoryName(solutionPath);
147 |                     if (!string.IsNullOrEmpty(solutionDir)) {
148 |                         await editorConfigProvider.InitializeAsync(solutionDir, CancellationToken.None);
149 |                         Log.Information("Solution loaded successfully: {SolutionPath}", solutionPath);
150 |                     } else {
151 |                         Log.Warning("Could not determine directory for solution path: {SolutionPath}", solutionPath);
152 |                     }
153 |                 } catch (Exception ex) {
154 |                     Log.Error(ex, "Error loading solution: {SolutionPath}", solutionPath);
155 |                 }
156 |             }
157 | 
158 |             await host.RunAsync();
159 |             return 0;
160 |         } catch (Exception ex) {
161 |             Log.Fatal(ex, "{AppName} terminated unexpectedly.", ApplicationName);
162 |             return 1;
163 |         } finally {
164 |             Log.Information("{AppName} shutting down.", ApplicationName);
165 |             await Log.CloseAndFlushAsync();
166 |         }
167 |     }
168 | }
169 | 
170 | 
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/EmbeddedSourceReader.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Linq;
  5 | using System.Reflection.Metadata;
  6 | using System.Reflection.PortableExecutable;
  7 | using System.Text;
  8 | using System.IO.Compression;
  9 | using Microsoft.CodeAnalysis;
 10 | 
 11 | namespace SharpTools.Tools.Services {
 12 |     public class EmbeddedSourceReader {
 13 |         // GUID for embedded source custom debug information
 14 |         private static readonly Guid EmbeddedSourceGuid = new Guid("0E8A571B-6926-466E-B4AD-8AB04611F5FE");
 15 | 
 16 |         public class SourceResult {
 17 |             public string? SourceCode { get; set; }
 18 |             public string? FilePath { get; set; }
 19 |             public bool IsEmbedded { get; set; }
 20 |             public bool IsCompressed { get; set; }
 21 |         }
 22 | 
 23 |         /// <summary>
 24 |         /// Reads embedded source from a portable PDB file
 25 |         /// </summary>
 26 |         public static Dictionary<string, SourceResult> ReadEmbeddedSources(string pdbPath) {
 27 |             var results = new Dictionary<string, SourceResult>();
 28 | 
 29 |             using var fs = new FileStream(pdbPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 30 |             using var provider = MetadataReaderProvider.FromPortablePdbStream(fs);
 31 |             var reader = provider.GetMetadataReader();
 32 | 
 33 |             return ReadEmbeddedSources(reader);
 34 |         }
 35 | 
 36 |         /// <summary>
 37 |         /// Reads embedded source from an assembly with embedded PDB
 38 |         /// </summary>
 39 |         public static Dictionary<string, SourceResult> ReadEmbeddedSourcesFromAssembly(string assemblyPath) {
 40 |             using var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 41 |             using var peReader = new PEReader(fs);
 42 | 
 43 |             // Check for embedded portable PDB
 44 |             var debugDirectories = peReader.ReadDebugDirectory();
 45 |             var embeddedPdbEntry = debugDirectories
 46 |                 .FirstOrDefault(entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
 47 | 
 48 |             if (embeddedPdbEntry.DataSize == 0) {
 49 |                 return new Dictionary<string, SourceResult>();
 50 |             }
 51 | 
 52 |             using var embeddedProvider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry);
 53 |             var pdbReader = embeddedProvider.GetMetadataReader();
 54 | 
 55 |             return ReadEmbeddedSources(pdbReader);
 56 |         }
 57 | 
 58 |         /// <summary>
 59 |         /// Core method to read embedded sources from a MetadataReader
 60 |         /// </summary>
 61 |         public static Dictionary<string, SourceResult> ReadEmbeddedSources(MetadataReader reader) {
 62 |             var results = new Dictionary<string, SourceResult>();
 63 | 
 64 |             // Get all documents
 65 |             var documents = new Dictionary<DocumentHandle, System.Reflection.Metadata.Document>();
 66 |             foreach (var docHandle in reader.Documents) {
 67 |                 var doc = reader.GetDocument(docHandle);
 68 |                 documents[docHandle] = doc;
 69 |             }
 70 | 
 71 |             // Look for embedded source in CustomDebugInformation
 72 |             foreach (var cdiHandle in reader.CustomDebugInformation) {
 73 |                 var cdi = reader.GetCustomDebugInformation(cdiHandle);
 74 | 
 75 |                 // Check if this is embedded source information
 76 |                 var kind = reader.GetGuid(cdi.Kind);
 77 |                 if (kind != EmbeddedSourceGuid)
 78 |                     continue;
 79 | 
 80 |                 // The parent should be a Document
 81 |                 if (cdi.Parent.Kind != HandleKind.Document)
 82 |                     continue;
 83 | 
 84 |                 var docHandle = (DocumentHandle)cdi.Parent;
 85 |                 if (!documents.TryGetValue(docHandle, out var document))
 86 |                     continue;
 87 | 
 88 |                 // Get the document name
 89 |                 var fileName = GetDocumentName(reader, document.Name);
 90 | 
 91 |                 // Read the embedded source content
 92 |                 var sourceContent = ReadEmbeddedSourceContent(reader, cdi.Value);
 93 | 
 94 |                 if (sourceContent != null) {
 95 |                     results[fileName] = sourceContent;
 96 |                 }
 97 |             }
 98 | 
 99 |             return results;
100 |         }
101 | 
102 |         /// <summary>
103 |         /// Reads the actual embedded source content from the blob
104 |         /// </summary>
105 |         private static SourceResult? ReadEmbeddedSourceContent(MetadataReader reader, BlobHandle blobHandle) {
106 |             var blobReader = reader.GetBlobReader(blobHandle);
107 | 
108 |             // Read the format indicator (first 4 bytes)
109 |             var format = blobReader.ReadInt32();
110 | 
111 |             // Get remaining bytes
112 |             var remainingLength = blobReader.Length - blobReader.Offset;
113 |             var contentBytes = blobReader.ReadBytes(remainingLength);
114 | 
115 |             string sourceText;
116 |             bool isCompressed = false;
117 | 
118 |             if (format == 0) {
119 |                 // Uncompressed UTF-8 text
120 |                 sourceText = Encoding.UTF8.GetString(contentBytes);
121 |             } else if (format > 0) {
122 |                 // Compressed with deflate, format contains uncompressed size
123 |                 isCompressed = true;
124 |                 using var compressed = new MemoryStream(contentBytes);
125 |                 using var deflate = new DeflateStream(compressed, CompressionMode.Decompress);
126 |                 using var decompressed = new MemoryStream();
127 | 
128 |                 deflate.CopyTo(decompressed);
129 |                 sourceText = Encoding.UTF8.GetString(decompressed.ToArray());
130 |             } else {
131 |                 // Reserved for future formats
132 |                 return null;
133 |             }
134 | 
135 |             return new SourceResult {
136 |                 SourceCode = sourceText,
137 |                 IsEmbedded = true,
138 |                 IsCompressed = isCompressed
139 |             };
140 |         }
141 | 
142 |         /// <summary>
143 |         /// Reconstructs the document name from the portable PDB format
144 |         /// </summary>
145 |         private static string GetDocumentName(MetadataReader reader, DocumentNameBlobHandle handle) {
146 |             var blobReader = reader.GetBlobReader(handle);
147 |             var separator = (char)blobReader.ReadByte();
148 | 
149 |             var sb = new StringBuilder();
150 |             bool first = true;
151 | 
152 |             while (blobReader.Offset < blobReader.Length) {
153 |                 var partHandle = blobReader.ReadBlobHandle();
154 |                 if (!partHandle.IsNil) {
155 |                     if (!first)
156 |                         sb.Append(separator);
157 | 
158 |                     var nameBytes = reader.GetBlobBytes(partHandle);
159 |                     sb.Append(Encoding.UTF8.GetString(nameBytes));
160 |                     first = false;
161 |                 }
162 |             }
163 | 
164 |             return sb.ToString();
165 |         }
166 |         /// <summary>
167 |         /// Helper method to get source for a specific symbol from Roslyn
168 |         /// </summary>
169 |         public static SourceResult? GetEmbeddedSourceForSymbol(Microsoft.CodeAnalysis.ISymbol symbol) {
170 |             // Get the assembly containing the symbol
171 |             var assembly = symbol.ContainingAssembly;
172 |             if (assembly == null)
173 |                 return null;
174 | 
175 |             // Get the locations from the symbol
176 |             var locations = symbol.Locations;
177 |             foreach (var location in locations) {
178 |                 if (location.IsInMetadata && location.MetadataModule != null) {
179 |                     var moduleName = location.MetadataModule.Name;
180 | 
181 |                     // Try to find the defining document for this symbol
182 |                     string symbolFileName = moduleName;
183 | 
184 |                     // For types, properties, methods, etc., use a more specific name
185 |                     if (symbol is Microsoft.CodeAnalysis.INamedTypeSymbol namedType) {
186 |                         symbolFileName = $"{namedType.Name}.cs";
187 |                     } else if (symbol.ContainingType != null) {
188 |                         symbolFileName = $"{symbol.ContainingType.Name}.cs";
189 |                     }
190 | 
191 |                     // Check if we can find embedded source for this symbol
192 |                     // The actual PDB path lookup will be handled by the calling code
193 |                     return new SourceResult {
194 |                         FilePath = symbolFileName,
195 |                         IsEmbedded = true,
196 |                         IsCompressed = false
197 |                     };
198 |                 }
199 |             }
200 | 
201 |             // If we reach here, we couldn't determine the assembly location directly
202 |             return null;
203 |         }
204 |     }
205 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/LegacyNuGetPackageReader.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Linq;
  5 | using System.Xml;
  6 | using System.Xml.Linq;
  7 | 
  8 | namespace SharpTools.Tools.Services;
  9 | 
 10 | /// <summary>
 11 | /// Comprehensive NuGet package reader supporting both PackageReference and packages.config
 12 | /// </summary>
 13 | public class LegacyNuGetPackageReader {
 14 |     public class PackageReference {
 15 |         public string PackageId { get; set; } = string.Empty;
 16 |         public string Version { get; set; } = string.Empty;
 17 |         public string? TargetFramework { get; set; }
 18 |         public bool IsDevelopmentDependency { get; set; }
 19 |         public PackageFormat Format { get; set; }
 20 |         public string? HintPath { get; set; } // For packages.config references
 21 |     }
 22 | 
 23 |     public enum PackageFormat {
 24 |         PackageReference,
 25 |         PackagesConfig
 26 |     }
 27 | 
 28 |     public class ProjectPackageInfo {
 29 |         public string ProjectPath { get; set; } = string.Empty;
 30 |         public PackageFormat Format { get; set; }
 31 |         public List<PackageReference> Packages { get; set; } = new List<PackageReference>();
 32 |         public string? PackagesConfigPath { get; set; }
 33 |     }
 34 | 
 35 |     /// <summary>
 36 |     /// Gets package information for a project, automatically detecting the format
 37 |     /// </summary>
 38 |     public static ProjectPackageInfo GetPackagesForProject(string projectPath) {
 39 |         var info = new ProjectPackageInfo {
 40 |             ProjectPath = projectPath,
 41 |             Format = DetectPackageFormat(projectPath)
 42 |         };
 43 | 
 44 |         if (info.Format == PackageFormat.PackageReference) {
 45 |             info.Packages = GetPackageReferences(projectPath);
 46 |         } else {
 47 |             info.PackagesConfigPath = GetPackagesConfigPath(projectPath);
 48 |             info.Packages = GetPackagesFromConfig(info.PackagesConfigPath);
 49 |         }
 50 | 
 51 |         return info;
 52 |     }
 53 | 
 54 |     /// <summary>
 55 |     /// Gets basic package information without using MSBuild (used as fallback)
 56 |     /// </summary>
 57 |     public static List<PackageReference> GetBasicPackageReferencesWithoutMSBuild(string projectPath) {
 58 |         var packages = new List<PackageReference>();
 59 | 
 60 |         try {
 61 |             if (!File.Exists(projectPath)) {
 62 |                 return packages;
 63 |             }
 64 | 
 65 |             var xDoc = XDocument.Load(projectPath);
 66 |             var packageRefs = xDoc.Descendants("PackageReference");
 67 | 
 68 |             foreach (var packageRef in packageRefs) {
 69 |                 string? packageId = packageRef.Attribute("Include")?.Value;
 70 |                 string? version = packageRef.Attribute("Version")?.Value;
 71 | 
 72 |                 // If version is not in attribute, check for Version element
 73 |                 if (string.IsNullOrEmpty(version)) {
 74 |                     version = packageRef.Element("Version")?.Value;
 75 |                 }
 76 | 
 77 |                 if (!string.IsNullOrEmpty(packageId) && !string.IsNullOrEmpty(version)) {
 78 |                     packages.Add(new PackageReference {
 79 |                         PackageId = packageId,
 80 |                         Version = version,
 81 |                         Format = PackageFormat.PackageReference
 82 |                     });
 83 |                 }
 84 |             }
 85 |         } catch (Exception) {
 86 |             // Ignore errors and return what we have
 87 |         }
 88 | 
 89 |         return packages;
 90 |     }
 91 | 
 92 |     /// <summary>
 93 |     /// Detects whether a project uses PackageReference or packages.config
 94 |     /// </summary>
 95 |     public static PackageFormat DetectPackageFormat(string projectPath) {
 96 |         if (string.IsNullOrEmpty(projectPath) || !File.Exists(projectPath)) {
 97 |             return PackageFormat.PackageReference; // Default to modern format
 98 |         }
 99 | 
100 |         var projectDir = Path.GetDirectoryName(projectPath);
101 |         if (projectDir != null) {
102 |             var packagesConfigPath = Path.Combine(projectDir, "packages.config");
103 | 
104 |             // Check if packages.config exists
105 |             if (File.Exists(packagesConfigPath)) {
106 |                 return PackageFormat.PackagesConfig;
107 |             }
108 |         }
109 | 
110 |         // Check if project file contains PackageReference items using XML parsing
111 |         try {
112 |             var xDoc = XDocument.Load(projectPath);
113 |             var hasPackageReference = xDoc.Descendants("PackageReference").Any();
114 |             return hasPackageReference ? PackageFormat.PackageReference : PackageFormat.PackagesConfig;
115 |         } catch {
116 |             // If we can't load the project, assume packages.config for legacy projects
117 |             return PackageFormat.PackagesConfig;
118 |         }
119 |     }
120 | 
121 |     /// <summary>
122 |     /// Gets PackageReference items from modern SDK-style projects
123 |     /// </summary>
124 |     public static List<PackageReference> GetPackageReferences(string projectPath) {
125 |         // Use XML parsing approach instead of MSBuild API
126 |         return GetBasicPackageReferencesWithoutMSBuild(projectPath);
127 |     }
128 | 
129 |     /// <summary>
130 |     /// Gets package information from packages.config file
131 |     /// </summary>
132 |     public static List<PackageReference> GetPackagesFromConfig(string? packagesConfigPath) {
133 |         var packages = new List<PackageReference>();
134 | 
135 |         if (string.IsNullOrEmpty(packagesConfigPath) || !File.Exists(packagesConfigPath)) {
136 |             return packages;
137 |         }
138 | 
139 |         try {
140 |             var doc = XDocument.Load(packagesConfigPath);
141 |             var packageElements = doc.Root?.Elements("package");
142 | 
143 |             if (packageElements != null) {
144 |                 foreach (var packageElement in packageElements) {
145 |                     var packageId = packageElement.Attribute("id")?.Value;
146 |                     var version = packageElement.Attribute("version")?.Value;
147 |                     var targetFramework = packageElement.Attribute("targetFramework")?.Value;
148 |                     var isDevelopmentDependency = string.Equals(
149 |                         packageElement.Attribute("developmentDependency")?.Value, "true",
150 |                         StringComparison.OrdinalIgnoreCase);
151 | 
152 |                     if (!string.IsNullOrEmpty(packageId) && !string.IsNullOrEmpty(version)) {
153 |                         packages.Add(new PackageReference {
154 |                             PackageId = packageId,
155 |                             Version = version,
156 |                             TargetFramework = targetFramework,
157 |                             IsDevelopmentDependency = isDevelopmentDependency,
158 |                             Format = PackageFormat.PackagesConfig
159 |                         });
160 |                     }
161 |                 }
162 |             }
163 |         } catch {
164 |             // Return empty list if parsing fails
165 |         }
166 | 
167 |         return packages;
168 |     }
169 | 
170 |     /// <summary>
171 |     /// Gets the packages.config path for a project
172 |     /// </summary>
173 |     public static string GetPackagesConfigPath(string projectPath) {
174 |         if (string.IsNullOrEmpty(projectPath)) {
175 |             return string.Empty;
176 |         }
177 | 
178 |         var projectDir = Path.GetDirectoryName(projectPath);
179 |         return string.IsNullOrEmpty(projectDir) ? string.Empty : Path.Combine(projectDir, "packages.config");
180 |     }
181 | 
182 |     /// <summary>
183 |     /// Gets all packages from a project file regardless of format
184 |     /// </summary>
185 |     public static List<(string PackageId, string Version)> GetAllPackages(string projectPath) {
186 |         if (string.IsNullOrEmpty(projectPath) || !File.Exists(projectPath)) {
187 |             return new List<(string, string)>();
188 |         }
189 | 
190 |         var packages = new List<(string, string)>();
191 |         var format = DetectPackageFormat(projectPath);
192 | 
193 |         try {
194 |             if (format == PackageFormat.PackageReference) {
195 |                 var packageRefs = GetPackageReferences(projectPath);
196 |                 packages.AddRange(packageRefs.Select(p => (p.PackageId, p.Version)));
197 |             } else {
198 |                 var packagesConfigPath = GetPackagesConfigPath(projectPath);
199 |                 var packageRefs = GetPackagesFromConfig(packagesConfigPath);
200 |                 packages.AddRange(packageRefs.Select(p => (p.PackageId, p.Version)));
201 |             }
202 |         } catch {
203 |             // Return what we have if an error occurs
204 |         }
205 | 
206 |         return packages;
207 |     }
208 |     public static List<PackageReference> GetAllPackageReferences(string projectPath) {
209 |         if (string.IsNullOrEmpty(projectPath) || !File.Exists(projectPath)) {
210 |             return new List<PackageReference>();
211 |         }
212 | 
213 |         var format = DetectPackageFormat(projectPath);
214 | 
215 |         try {
216 |             if (format == PackageFormat.PackageReference) {
217 |                 return GetPackageReferences(projectPath);
218 |             } else {
219 |                 var packagesConfigPath = GetPackagesConfigPath(projectPath);
220 |                 return GetPackagesFromConfig(packagesConfigPath);
221 |             }
222 |         } catch {
223 |             // Return empty list if an error occurs
224 |             return new List<PackageReference>();
225 |         }
226 |     }
227 | }
```

--------------------------------------------------------------------------------
/SharpTools.SseServer/Program.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using SharpTools.Tools.Services;
  2 | using SharpTools.Tools.Interfaces;
  3 | using SharpTools.Tools.Mcp.Tools;
  4 | using SharpTools.Tools.Extensions;
  5 | using System.CommandLine;
  6 | using System.CommandLine.Parsing;
  7 | using Microsoft.AspNetCore.HttpLogging;
  8 | using Serilog;
  9 | using ModelContextProtocol.Protocol;
 10 | using System.Reflection;
 11 | namespace SharpTools.SseServer;
 12 | 
 13 | using SharpTools.Tools.Services;
 14 | using SharpTools.Tools.Interfaces;
 15 | using SharpTools.Tools.Mcp.Tools;
 16 | using System.CommandLine;
 17 | using System.CommandLine.Parsing;
 18 | using Microsoft.AspNetCore.HttpLogging;
 19 | using Serilog;
 20 | using ModelContextProtocol.Protocol;
 21 | using System.Reflection;
 22 | 
 23 | public class Program {
 24 |     // --- Application ---
 25 |     public const string ApplicationName = "SharpToolsMcpSseServer";
 26 |     public const string ApplicationVersion = "0.0.1";
 27 |     public const string LogOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}";
 28 |     public static async Task<int> Main(string[] args) {
 29 |         // Ensure tool assemblies are loaded for MCP SDK's WithToolsFromAssembly
 30 |         _ = typeof(SolutionTools);
 31 |         _ = typeof(AnalysisTools);
 32 |         _ = typeof(ModificationTools);
 33 | 
 34 |         var portOption = new Option<int>("--port") {
 35 |             Description = "The port number for the MCP server to listen on.",
 36 |             DefaultValueFactory = x => 3001
 37 |         };
 38 | 
 39 |         var logFileOption = new Option<string?>("--log-file") {
 40 |             Description = "Optional path to a log file. If not specified, logs only go to console."
 41 |         };
 42 | 
 43 |         var logLevelOption = new Option<Serilog.Events.LogEventLevel>("--log-level") {
 44 |             Description = "Minimum log level for console and file.",
 45 |             DefaultValueFactory = x => Serilog.Events.LogEventLevel.Information
 46 |         };
 47 | 
 48 |         var loadSolutionOption = new Option<string?>("--load-solution") {
 49 |             Description = "Path to a solution file (.sln) to load immediately on startup."
 50 |         };
 51 | 
 52 |         var buildConfigurationOption = new Option<string?>("--build-configuration") {
 53 |             Description = "Build configuration to use when loading the solution (Debug, Release, etc.)."
 54 |         };
 55 | 
 56 |         var disableGitOption = new Option<bool>("--disable-git") {
 57 |             Description = "Disable Git integration.",
 58 |             DefaultValueFactory = x => false
 59 |         };
 60 | 
 61 |         var rootCommand = new RootCommand("SharpTools MCP Server") {
 62 |             portOption,
 63 |             logFileOption,
 64 |             logLevelOption,
 65 |             loadSolutionOption,
 66 |             buildConfigurationOption,
 67 |             disableGitOption
 68 |         };
 69 | 
 70 |         ParseResult? parseResult = rootCommand.Parse(args);
 71 |         if (parseResult == null) {
 72 |             Console.Error.WriteLine("Failed to parse command line arguments.");
 73 |             return 1;
 74 |         }
 75 | 
 76 |         int port = parseResult.GetValue(portOption);
 77 |         string? logFilePath = parseResult.GetValue(logFileOption);
 78 |         Serilog.Events.LogEventLevel minimumLogLevel = parseResult.GetValue(logLevelOption);
 79 |         string? solutionPath = parseResult.GetValue(loadSolutionOption);
 80 |         string? buildConfiguration = parseResult.GetValue(buildConfigurationOption)!;
 81 |         bool disableGit = parseResult.GetValue(disableGitOption);
 82 |         string serverUrl = $"http://localhost:{port}";
 83 | 
 84 |         var loggerConfiguration = new LoggerConfiguration()
 85 |             .MinimumLevel.Is(minimumLogLevel) // Set based on command line
 86 |             .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) // Default override
 87 |             .MinimumLevel.Override("Microsoft.Hosting.Lifetime", Serilog.Events.LogEventLevel.Information)
 88 |             // For debugging connection issues, set AspNetCore to Information or Debug
 89 |             .MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Information)
 90 |             .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", Serilog.Events.LogEventLevel.Information)
 91 |             .MinimumLevel.Override("Microsoft.AspNetCore.Routing", Serilog.Events.LogEventLevel.Information)
 92 |             .MinimumLevel.Override("Microsoft.AspNetCore.Server.Kestrel", Serilog.Events.LogEventLevel.Debug) // Kestrel connection logs
 93 |             .MinimumLevel.Override("Microsoft.CodeAnalysis", Serilog.Events.LogEventLevel.Information)
 94 |             .MinimumLevel.Override("ModelContextProtocol", Serilog.Events.LogEventLevel.Warning)
 95 |             .Enrich.FromLogContext()
 96 |             .WriteTo.Async(a => a.Console(
 97 |                 outputTemplate: LogOutputTemplate,
 98 |                 standardErrorFromLevel: Serilog.Events.LogEventLevel.Verbose,
 99 |                 restrictedToMinimumLevel: minimumLogLevel));
100 | 
101 |         if (!string.IsNullOrWhiteSpace(logFilePath)) {
102 |             var logDirectory = Path.GetDirectoryName(logFilePath);
103 |             if (!string.IsNullOrWhiteSpace(logDirectory) && !Directory.Exists(logDirectory)) {
104 |                 Directory.CreateDirectory(logDirectory);
105 |             }
106 |             loggerConfiguration.WriteTo.Async(a => a.File(
107 |                 logFilePath,
108 |                 rollingInterval: RollingInterval.Day,
109 |                 outputTemplate: LogOutputTemplate,
110 |                 fileSizeLimitBytes: 10 * 1024 * 1024,
111 |                 rollOnFileSizeLimit: true,
112 |                 retainedFileCountLimit: 7,
113 |                 restrictedToMinimumLevel: minimumLogLevel));
114 |             Console.WriteLine($"Logging to file: {Path.GetFullPath(logFilePath)} with minimum level {minimumLogLevel}");
115 |         }
116 | 
117 |         Log.Logger = loggerConfiguration.CreateBootstrapLogger();
118 | 
119 |         if (disableGit) {
120 |             Log.Information("Git integration is disabled.");
121 |         }
122 | 
123 |         if (!string.IsNullOrEmpty(buildConfiguration)) {
124 |             Log.Information("Using build configuration: {BuildConfiguration}", buildConfiguration);
125 |         }
126 | 
127 |         try {
128 |             Log.Information("Configuring {AppName} v{AppVersion} to run on {ServerUrl} with minimum log level {LogLevel}",
129 |                 ApplicationName, ApplicationVersion, serverUrl, minimumLogLevel);
130 | 
131 |             var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = args });
132 | 
133 |             builder.Host.UseSerilog();
134 | 
135 |             // Add W3CLogging for detailed HTTP request logging
136 |             // This logs to Microsoft.Extensions.Logging, which Serilog will capture.
137 |             builder.Services.AddW3CLogging(logging => {
138 |                 logging.LoggingFields = W3CLoggingFields.All; // Log all available fields
139 |                 logging.FileSizeLimit = 5 * 1024 * 1024; // 5 MB
140 |                 logging.RetainedFileCountLimit = 2;
141 |                 logging.FileName = "access-"; // Prefix for log files
142 |                                               // By default, logs to a 'logs' subdirectory of the app's content root.
143 |                                               // Can be configured: logging.RootPath = ...
144 |             });
145 | 
146 |             builder.Services.WithSharpToolsServices(!disableGit, buildConfiguration);
147 | 
148 |             builder.Services
149 |                 .AddMcpServer(options => {
150 |                     options.ServerInfo = new Implementation {
151 |                         Name = ApplicationName,
152 |                         Version = ApplicationVersion,
153 |                     };
154 |                     // For debugging, you can hook into handlers here if needed,
155 |                     // but ModelContextProtocol's own Debug logging should be sufficient.
156 |                 })
157 |                 .WithHttpTransport()
158 |                 .WithSharpTools();
159 | 
160 |             var app = builder.Build();
161 | 
162 |             // Load solution if specified in command line arguments
163 |             if (!string.IsNullOrEmpty(solutionPath)) {
164 |                 try {
165 |                     var solutionManager = app.Services.GetRequiredService<ISolutionManager>();
166 |                     var editorConfigProvider = app.Services.GetRequiredService<IEditorConfigProvider>();
167 | 
168 |                     Log.Information("Loading solution: {SolutionPath}", solutionPath);
169 |                     await solutionManager.LoadSolutionAsync(solutionPath, CancellationToken.None);
170 | 
171 |                     var solutionDir = Path.GetDirectoryName(solutionPath);
172 |                     if (!string.IsNullOrEmpty(solutionDir)) {
173 |                         await editorConfigProvider.InitializeAsync(solutionDir, CancellationToken.None);
174 |                         Log.Information("Solution loaded successfully: {SolutionPath}", solutionPath);
175 |                     } else {
176 |                         Log.Warning("Could not determine directory for solution path: {SolutionPath}", solutionPath);
177 |                     }
178 |                 } catch (Exception ex) {
179 |                     Log.Error(ex, "Error loading solution: {SolutionPath}", solutionPath);
180 |                 }
181 |             }
182 | 
183 |             // --- ASP.NET Core Middleware ---
184 | 
185 |             // 1. W3C Logging Middleware (if enabled and configured to log to a file separate from Serilog)
186 |             //    If W3CLogging is configured to write to files, it has its own middleware.
187 |             //    If it's just for ILogger, Serilog picks it up.
188 |             // app.UseW3CLogging(); // This is needed if W3CLogging is writing its own files.
189 |             // If it's just feeding ILogger, Serilog handles it.
190 | 
191 |             // 2. Custom Request Logging Middleware (very early in the pipeline)
192 |             app.Use(async (context, next) => {
193 |                 var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
194 |                 logger.LogDebug("Incoming Request: {Method} {Path} {QueryString} from {RemoteIpAddress}",
195 |                     context.Request.Method,
196 |                     context.Request.Path,
197 |                     context.Request.QueryString,
198 |                     context.Connection.RemoteIpAddress);
199 | 
200 |                 // Log headers for more detail if needed (can be verbose)
201 |                 // foreach (var header in context.Request.Headers) {
202 |                 //     logger.LogTrace("Header: {Key}: {Value}", header.Key, header.Value);
203 |                 // }
204 |                 try {
205 |                     await next(context);
206 |                 } catch (Exception ex) {
207 |                     logger.LogError(ex, "Error processing request: {Method} {Path}", context.Request.Method, context.Request.Path);
208 |                     throw; // Re-throw to let ASP.NET Core handle it
209 |                 }
210 | 
211 |                 logger.LogDebug("Outgoing Response: {StatusCode} for {Method} {Path}",
212 |                     context.Response.StatusCode,
213 |                     context.Request.Method,
214 |                     context.Request.Path);
215 |             });
216 | 
217 | 
218 |             // 3. Standard ASP.NET Core middleware (HTTPS redirection, routing, auth, etc. - not used here yet)
219 |             // if (app.Environment.IsDevelopment()) { }
220 |             // app.UseHttpsRedirection(); 
221 | 
222 |             // 4. MCP Middleware
223 |             app.MapMcp(); // Maps the MCP endpoint (typically "/mcp")
224 | 
225 |             Log.Information("Starting {AppName} server...", ApplicationName);
226 |             await app.RunAsync(serverUrl);
227 | 
228 |             return 0;
229 | 
230 |         } catch (Exception ex) {
231 |             Log.Fatal(ex, "{AppName} terminated unexpectedly.", ApplicationName);
232 |             return 1;
233 |         } finally {
234 |             Log.Information("{AppName} shutting down.", ApplicationName);
235 |             await Log.CloseAndFlushAsync();
236 |         }
237 |     }
238 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Mcp/Tools/PackageTools.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Microsoft.Extensions.Logging;
  2 | using ModelContextProtocol;
  3 | using NuGet.Common;
  4 | using NuGet.Protocol;
  5 | using NuGet.Protocol.Core.Types;
  6 | using NuGet.Versioning;
  7 | using SharpTools.Tools.Services;
  8 | using System.Xml.Linq;
  9 | 
 10 | namespace SharpTools.Tools.Mcp.Tools;
 11 | 
 12 | // Marker class for ILogger<T> category specific to PackageTools
 13 | public class PackageToolsLogCategory { }
 14 | 
 15 | [McpServerToolType]
 16 | public static class PackageTools {
 17 |     // Disabled for now, needs to handle dependencies and reloading solution
 18 |     //[McpServerTool(Name = ToolHelpers.SharpToolPrefix + nameof(AddOrModifyNugetPackage), Idempotent = false, ReadOnly = false, Destructive = false, OpenWorld = false)]
 19 |     [Description("Adds or modifies a NuGet package in a project.")]
 20 |     public static async Task<string> AddOrModifyNugetPackage(
 21 |         ILogger<PackageToolsLogCategory> logger,
 22 |         ISolutionManager solutionManager,
 23 |         IDocumentOperationsService documentOperations,
 24 |         string projectName,
 25 |         string nugetPackageId,
 26 |         [Description("The version of the NuGet package or 'latest' for latest")] string? version,
 27 |         CancellationToken cancellationToken = default) {
 28 |         return await ErrorHandlingHelpers.ExecuteWithErrorHandlingAsync(async () => {
 29 |             // Validate parameters
 30 |             ErrorHandlingHelpers.ValidateStringParameter(projectName, nameof(projectName), logger);
 31 |             ErrorHandlingHelpers.ValidateStringParameter(nugetPackageId, nameof(nugetPackageId), logger);
 32 |             logger.LogInformation("Adding/modifying NuGet package '{PackageId}' {Version} to {ProjectPath}",
 33 |                 nugetPackageId, version ?? "latest", projectName);
 34 | 
 35 |             if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) {
 36 |                 version = null; // Treat 'latest' as null for processing
 37 |             }
 38 | 
 39 |             int indexOfParen = projectName.IndexOf('(');
 40 |             string projectNameNormalized = indexOfParen == -1
 41 |                 ? projectName.Trim()
 42 |                 : projectName[..indexOfParen].Trim();
 43 | 
 44 |             var project = solutionManager.GetProjects().FirstOrDefault(
 45 |                 p => p.Name == projectName
 46 |                 || p.AssemblyName == projectName
 47 |                 || p.Name == projectNameNormalized);
 48 | 
 49 |             if (project == null) {
 50 |                 logger.LogError("Project '{ProjectName}' not found in the loaded solution", projectName);
 51 |                 throw new McpException($"Project '{projectName}' not found in the solution.");
 52 |             }
 53 | 
 54 |             // Validate the package exists
 55 |             var packageExists = await ValidatePackageAsync(nugetPackageId, version, logger, cancellationToken);
 56 |             if (!packageExists) {
 57 |                 throw new McpException($"Package '{nugetPackageId}' {(string.IsNullOrEmpty(version) ? "" : $"with version {version} ")}was not found on NuGet.org.");
 58 |             }
 59 | 
 60 |             // If no version specified, get the latest version
 61 |             if (string.IsNullOrEmpty(version)) {
 62 |                 version = await GetLatestVersionAsync(nugetPackageId, logger, cancellationToken);
 63 |                 logger.LogInformation("Using latest version {Version} for package {PackageId}", version, nugetPackageId);
 64 |             }
 65 | 
 66 |             ErrorHandlingHelpers.ValidateFileExists(project.FilePath, logger);
 67 |             var projectPath = project.FilePath!;
 68 | 
 69 |             // Detect package format and add/update accordingly
 70 |             var packageFormat = LegacyNuGetPackageReader.DetectPackageFormat(projectPath);
 71 |             var action = "added";
 72 |             var projectPackages = LegacyNuGetPackageReader.GetPackagesForProject(projectPath);
 73 | 
 74 |             // Check if package already exists to determine if we're adding or updating
 75 |             var existingPackage = projectPackages.Packages.FirstOrDefault(p =>
 76 |                 string.Equals(p.PackageId, nugetPackageId, StringComparison.OrdinalIgnoreCase));
 77 | 
 78 |             if (existingPackage != null) {
 79 |                 action = "updated";
 80 |                 logger.LogInformation("Package {PackageId} already exists with version {OldVersion}, updating to {NewVersion}",
 81 |                     nugetPackageId, existingPackage.Version, version);
 82 |             }
 83 | 
 84 |             // Update the project file based on the package format
 85 |             if (packageFormat == LegacyNuGetPackageReader.PackageFormat.PackageReference) {
 86 |                 await UpdatePackageReferenceAsync(projectPath, nugetPackageId, version, existingPackage != null, documentOperations, logger, cancellationToken);
 87 |             } else {
 88 |                 var packagesConfigPath = projectPackages.PackagesConfigPath ?? throw new McpException("packages.config path not found.");
 89 |                 await UpdatePackagesConfigAsync(packagesConfigPath, nugetPackageId, version, existingPackage != null, documentOperations, logger, cancellationToken);
 90 |             }
 91 | 
 92 |             logger.LogInformation("Package {PackageId} {Action} with version {Version}", nugetPackageId, action, version);
 93 |             return $"The package {nugetPackageId} has been {action} with version {version}. You must perform a `{(packageFormat == LegacyNuGetPackageReader.PackageFormat.PackageReference ? "dotnet restore" : "nuget restore")}` and then reload the solution.";
 94 |         }, logger, nameof(AddOrModifyNugetPackage), cancellationToken);
 95 |     }
 96 |     private static async Task<bool> ValidatePackageAsync(string packageId, string? version, Microsoft.Extensions.Logging.ILogger logger, CancellationToken cancellationToken) {
 97 |         try {
 98 |             logger.LogInformation("Validating package {PackageId} {Version} on NuGet.org",
 99 |                 packageId, version ?? "latest");
100 | 
101 |             var nugetLogger = NullLogger.Instance;
102 | 
103 |             // Create repository
104 |             var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json");
105 |             var resource = await repository.GetResourceAsync<PackageMetadataResource>(cancellationToken);
106 | 
107 |             // Get package metadata
108 |             var packages = await resource.GetMetadataAsync(
109 |                 packageId,
110 |                 includePrerelease: true,
111 |                 includeUnlisted: false,
112 |                 sourceCacheContext: new SourceCacheContext(),
113 |                 nugetLogger,
114 |                 cancellationToken);
115 | 
116 |             if (!packages.Any())
117 |                 return false; // Package doesn't exist
118 | 
119 |             if (string.IsNullOrEmpty(version))
120 |                 return true; // Just checking existence
121 | 
122 |             // Validate specific version
123 |             var targetVersion = NuGetVersion.Parse(version);
124 |             return packages.Any(p => p.Identity.Version.Equals(targetVersion));
125 |         } catch (Exception ex) {
126 |             logger.LogError(ex, "Error validating NuGet package {PackageId} {Version}", packageId, version);
127 |             throw new McpException($"Failed to validate NuGet package '{packageId}': {ex.Message}");
128 |         }
129 |     }
130 | 
131 |     private static async Task<string> GetLatestVersionAsync(string packageId, Microsoft.Extensions.Logging.ILogger logger, CancellationToken cancellationToken) {
132 |         try {
133 |             var nugetLogger = NullLogger.Instance;
134 | 
135 |             var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json");
136 |             var resource = await repository.GetResourceAsync<PackageMetadataResource>(cancellationToken);
137 | 
138 |             var packages = await resource.GetMetadataAsync(
139 |                 packageId,
140 |                 includePrerelease: false, // Only stable versions for the latest
141 |                 includeUnlisted: false,
142 |                 sourceCacheContext: new SourceCacheContext(),
143 |                 nugetLogger,
144 |                 cancellationToken);
145 | 
146 |             var latestPackage = packages
147 |                 .OrderByDescending(p => p.Identity.Version)
148 |                 .FirstOrDefault();
149 | 
150 |             if (latestPackage == null) {
151 |                 throw new McpException($"No stable versions found for package '{packageId}'.");
152 |             }
153 | 
154 |             return latestPackage.Identity.Version.ToString();
155 |         } catch (Exception ex) {
156 |             logger.LogError(ex, "Error getting latest version for NuGet package {PackageId}", packageId);
157 |             throw new McpException($"Failed to get latest version for NuGet package '{packageId}': {ex.Message}");
158 |         }
159 |     }
160 | 
161 |     private static async Task UpdatePackageReferenceAsync(
162 |         string projectPath,
163 |         string packageId,
164 |         string version,
165 |         bool isUpdate,
166 |         IDocumentOperationsService documentOperations,
167 |         Microsoft.Extensions.Logging.ILogger logger,
168 |         CancellationToken cancellationToken) {
169 | 
170 |         try {
171 |             var (projectContent, _) = await documentOperations.ReadFileAsync(projectPath, false, cancellationToken);
172 |             var xDoc = XDocument.Parse(projectContent);
173 | 
174 |             // Find ItemGroup that contains PackageReference elements or create a new one
175 |             var itemGroup = xDoc.Root?.Elements("ItemGroup")
176 |                 .FirstOrDefault(ig => ig.Elements("PackageReference").Any());
177 | 
178 |             if (itemGroup == null) {
179 |                 // Create a new ItemGroup for PackageReferences
180 |                 itemGroup = new XElement("ItemGroup");
181 |                 xDoc.Root?.Add(itemGroup);
182 |             }
183 | 
184 |             // Find existing package reference
185 |             var existingPackage = itemGroup.Elements("PackageReference")
186 |                 .FirstOrDefault(pr => string.Equals(pr.Attribute("Include")?.Value, packageId, StringComparison.OrdinalIgnoreCase));
187 | 
188 |             if (existingPackage != null) {
189 |                 // Update existing package
190 |                 var versionAttr = existingPackage.Attribute("Version");
191 |                 if (versionAttr != null) {
192 |                     versionAttr.Value = version;
193 |                 } else {
194 |                     // Version might be in a child element
195 |                     var versionElement = existingPackage.Element("Version");
196 |                     if (versionElement != null) {
197 |                         versionElement.Value = version;
198 |                     } else {
199 |                         // Add version as attribute if neither exists
200 |                         existingPackage.Add(new XAttribute("Version", version));
201 |                     }
202 |                 }
203 |             } else {
204 |                 // Add new package reference
205 |                 var packageRef = new XElement("PackageReference",
206 |                     new XAttribute("Include", packageId),
207 |                     new XAttribute("Version", version));
208 | 
209 |                 itemGroup.Add(packageRef);
210 |             }
211 | 
212 |             // Save the updated project file
213 |             await documentOperations.WriteFileAsync(projectPath, xDoc.ToString(), true, cancellationToken,
214 |                 $"{(isUpdate ? "Updated" : "Added")} NuGet package {packageId} with version {version}");
215 |         } catch (Exception ex) {
216 |             logger.LogError(ex, "Error updating PackageReference in project file {ProjectPath}", projectPath);
217 |             throw new McpException($"Failed to update PackageReference in project file: {ex.Message}");
218 |         }
219 |     }
220 | 
221 |     private static async Task UpdatePackagesConfigAsync(
222 |         string? packagesConfigPath,
223 |         string packageId,
224 |         string version,
225 |         bool isUpdate,
226 |         IDocumentOperationsService documentOperations,
227 |         Microsoft.Extensions.Logging.ILogger logger,
228 |         CancellationToken cancellationToken) {
229 | 
230 |         if (string.IsNullOrEmpty(packagesConfigPath)) {
231 |             throw new McpException("packages.config path is null or empty.");
232 |         }
233 | 
234 |         try {
235 |             // Check if packages.config exists, if not create it
236 |             bool fileExists = documentOperations.FileExists(packagesConfigPath);
237 |             XDocument xDoc;
238 | 
239 |             if (fileExists) {
240 |                 var (content, _) = await documentOperations.ReadFileAsync(packagesConfigPath, false, cancellationToken);
241 |                 xDoc = XDocument.Parse(content);
242 |             } else {
243 |                 // Create a new packages.config file
244 |                 xDoc = new XDocument(
245 |                     new XElement("packages",
246 |                         new XAttribute("xmlns", "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd")));
247 |             }
248 | 
249 |             // Find existing package
250 |             var packageElement = xDoc.Root?.Elements("package")
251 |                 .FirstOrDefault(p => string.Equals(p.Attribute("id")?.Value, packageId, StringComparison.OrdinalIgnoreCase));
252 | 
253 |             if (packageElement != null) {
254 |                 // Update existing package
255 |                 packageElement.Attribute("version")!.Value = version;
256 |             } else {
257 |                 // Add new package entry
258 |                 var newPackage = new XElement("package",
259 |                     new XAttribute("id", packageId),
260 |                     new XAttribute("version", version),
261 |                     new XAttribute("targetFramework", "net40")); // Default target framework
262 | 
263 |                 xDoc.Root?.Add(newPackage);
264 |             }
265 | 
266 |             // Save the updated packages.config
267 |             await documentOperations.WriteFileAsync(packagesConfigPath, xDoc.ToString(), true, cancellationToken,
268 |                 $"{(isUpdate ? "Updated" : "Added")} NuGet package {packageId} with version {version} in packages.config");
269 |         } catch (Exception ex) {
270 |             logger.LogError(ex, "Error updating packages.config at {PackagesConfigPath}", packagesConfigPath);
271 |             throw new McpException($"Failed to update packages.config: {ex.Message}");
272 |         }
273 |     }
274 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/GitService.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using LibGit2Sharp;
  2 | using System.Text;
  3 | 
  4 | namespace SharpTools.Tools.Services;
  5 | 
  6 | public class GitService : IGitService {
  7 |     private readonly ILogger<GitService> _logger;
  8 |     private const string SharpToolsBranchPrefix = "sharptools/";
  9 |     private const string SharpToolsUndoBranchPrefix = "sharptools/undo/";
 10 | 
 11 |     public GitService(ILogger<GitService> logger) {
 12 |         _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 13 |     }
 14 | 
 15 |     public async Task<bool> IsRepositoryAsync(string solutionPath, CancellationToken cancellationToken = default) {
 16 |         return await Task.Run(() => {
 17 |             try {
 18 |                 var solutionDirectory = Path.GetDirectoryName(solutionPath);
 19 |                 if (string.IsNullOrEmpty(solutionDirectory)) {
 20 |                     return false;
 21 |                 }
 22 | 
 23 |                 var repositoryPath = Repository.Discover(solutionDirectory);
 24 |                 return !string.IsNullOrEmpty(repositoryPath);
 25 |             } catch (Exception ex) {
 26 |                 _logger.LogDebug("Error checking if path is a Git repository: {Error}", ex.Message);
 27 |                 return false;
 28 |             }
 29 |         }, cancellationToken);
 30 |     }
 31 | 
 32 |     public async Task<bool> IsOnSharpToolsBranchAsync(string solutionPath, CancellationToken cancellationToken = default) {
 33 |         return await Task.Run(() => {
 34 |             try {
 35 |                 var repositoryPath = GetRepositoryPath(solutionPath);
 36 |                 if (repositoryPath == null) {
 37 |                     return false;
 38 |                 }
 39 | 
 40 |                 using var repository = new Repository(repositoryPath);
 41 |                 var currentBranch = repository.Head.FriendlyName;
 42 |                 var isOnSharpToolsBranch = currentBranch.StartsWith(SharpToolsBranchPrefix, StringComparison.OrdinalIgnoreCase);
 43 | 
 44 |                 _logger.LogDebug("Current branch: {BranchName}, IsSharpToolsBranch: {IsSharpToolsBranch}",
 45 |                     currentBranch, isOnSharpToolsBranch);
 46 | 
 47 |                 return isOnSharpToolsBranch;
 48 |             } catch (Exception ex) {
 49 |                 _logger.LogWarning("Error checking current branch: {Error}", ex.Message);
 50 |                 return false;
 51 |             }
 52 |         }, cancellationToken);
 53 |     }
 54 | 
 55 |     public async Task EnsureSharpToolsBranchAsync(string solutionPath, CancellationToken cancellationToken = default) {
 56 |         await Task.Run(() => {
 57 |             try {
 58 |                 var repositoryPath = GetRepositoryPath(solutionPath);
 59 |                 if (repositoryPath == null) {
 60 |                     _logger.LogWarning("No Git repository found for solution at {SolutionPath}", solutionPath);
 61 |                     return;
 62 |                 }
 63 | 
 64 |                 using var repository = new Repository(repositoryPath);
 65 |                 var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd/HH-mm-ss");
 66 |                 var branchName = $"{SharpToolsBranchPrefix}{timestamp}";
 67 | 
 68 |                 // Check if we're already on a sharptools branch
 69 |                 var currentBranch = repository.Head.FriendlyName;
 70 |                 if (currentBranch.StartsWith(SharpToolsBranchPrefix, StringComparison.OrdinalIgnoreCase)) {
 71 |                     _logger.LogDebug("Already on SharpTools branch: {BranchName}", currentBranch);
 72 |                     return;
 73 |                 }
 74 | 
 75 |                 // Create and checkout the new branch
 76 |                 var newBranch = repository.CreateBranch(branchName);
 77 |                 Commands.Checkout(repository, newBranch);
 78 | 
 79 |                 _logger.LogInformation("Created and switched to SharpTools branch: {BranchName}", branchName);
 80 |             } catch (Exception ex) {
 81 |                 _logger.LogError(ex, "Error ensuring SharpTools branch for solution at {SolutionPath}", solutionPath);
 82 |                 throw;
 83 |             }
 84 |         }, cancellationToken);
 85 |     }
 86 | 
 87 |     public async Task CommitChangesAsync(string solutionPath, IEnumerable<string> changedFilePaths,
 88 |         string commitMessage, CancellationToken cancellationToken = default) {
 89 |         await Task.Run(() => {
 90 |             try {
 91 |                 var repositoryPath = GetRepositoryPath(solutionPath);
 92 |                 if (repositoryPath == null) {
 93 |                     _logger.LogWarning("No Git repository found for solution at {SolutionPath}", solutionPath);
 94 |                     return;
 95 |                 }
 96 | 
 97 |                 using var repository = new Repository(repositoryPath);
 98 | 
 99 |                 // Stage the changed files
100 |                 var stagedFiles = new List<string>();
101 |                 foreach (var filePath in changedFilePaths) {
102 |                     try {
103 |                         // Convert absolute path to relative path from repository root
104 |                         var relativePath = Path.GetRelativePath(repository.Info.WorkingDirectory, filePath);
105 | 
106 |                         // Stage the file
107 |                         Commands.Stage(repository, relativePath);
108 |                         stagedFiles.Add(relativePath);
109 | 
110 |                         _logger.LogDebug("Staged file: {FilePath}", relativePath);
111 |                     } catch (Exception ex) {
112 |                         _logger.LogWarning("Failed to stage file {FilePath}: {Error}", filePath, ex.Message);
113 |                     }
114 |                 }
115 | 
116 |                 if (stagedFiles.Count == 0) {
117 |                     _logger.LogWarning("No files were staged for commit");
118 |                     return;
119 |                 }
120 | 
121 |                 // Create commit
122 |                 var signature = GetCommitSignature(repository);
123 |                 var commit = repository.Commit(commitMessage, signature, signature);
124 | 
125 |                 _logger.LogInformation("Created commit {CommitSha} with {FileCount} files: {CommitMessage}",
126 |                     commit.Sha[..8], stagedFiles.Count, commitMessage);
127 |             } catch (Exception ex) {
128 |                 _logger.LogError(ex, "Error committing changes for solution at {SolutionPath}", solutionPath);
129 |                 throw;
130 |             }
131 |         }, cancellationToken);
132 |     }
133 | 
134 |     private string? GetRepositoryPath(string solutionPath) {
135 |         var solutionDirectory = Path.GetDirectoryName(solutionPath);
136 |         return string.IsNullOrEmpty(solutionDirectory) ? null : Repository.Discover(solutionDirectory);
137 |     }
138 | 
139 |     private Signature GetCommitSignature(Repository repository) {
140 |         try {
141 |             // Try to get user info from Git config
142 |             var config = repository.Config;
143 |             var name = config.Get<string>("user.name")?.Value ?? "SharpTools";
144 |             var email = config.Get<string>("user.email")?.Value ?? "sharptools@localhost";
145 | 
146 |             return new Signature(name, email, DateTimeOffset.Now);
147 |         } catch {
148 |             // Fallback to default signature
149 |             return new Signature("SharpTools", "sharptools@localhost", DateTimeOffset.Now);
150 |         }
151 |     }
152 | 
153 |     public async Task<string> CreateUndoBranchAsync(string solutionPath, CancellationToken cancellationToken = default) {
154 |         return await Task.Run(() => {
155 |             try {
156 |                 var repositoryPath = GetRepositoryPath(solutionPath);
157 |                 if (repositoryPath == null) {
158 |                     _logger.LogWarning("No Git repository found for solution at {SolutionPath}", solutionPath);
159 |                     return string.Empty;
160 |                 }
161 | 
162 |                 using var repository = new Repository(repositoryPath);
163 |                 var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd/HH-mm-ss");
164 |                 var branchName = $"{SharpToolsUndoBranchPrefix}{timestamp}";
165 | 
166 |                 // Create a new branch at the current commit, but don't checkout
167 |                 var currentCommit = repository.Head.Tip;
168 |                 var newBranch = repository.CreateBranch(branchName, currentCommit);
169 | 
170 |                 _logger.LogInformation("Created undo branch: {BranchName} at commit {CommitSha}",
171 |                     branchName, currentCommit.Sha[..8]);
172 | 
173 |                 return branchName;
174 |             } catch (Exception ex) {
175 |                 _logger.LogError(ex, "Error creating undo branch for solution at {SolutionPath}", solutionPath);
176 |                 return string.Empty;
177 |             }
178 |         }, cancellationToken);
179 |     }
180 | 
181 |     public async Task<string> GetDiffAsync(string solutionPath, string oldCommitSha, string newCommitSha, CancellationToken cancellationToken = default) {
182 |         return await Task.Run(() => {
183 |             try {
184 |                 var repositoryPath = GetRepositoryPath(solutionPath);
185 |                 if (repositoryPath == null) {
186 |                     _logger.LogWarning("No Git repository found for solution at {SolutionPath}", solutionPath);
187 |                     return string.Empty;
188 |                 }
189 | 
190 |                 using var repository = new Repository(repositoryPath);
191 |                 var oldCommit = repository.Lookup<Commit>(oldCommitSha);
192 |                 var newCommit = repository.Lookup<Commit>(newCommitSha);
193 | 
194 |                 if (oldCommit == null || newCommit == null) {
195 |                     _logger.LogWarning("Could not find commits for diff: Old {OldSha}, New {NewSha}",
196 |                         oldCommitSha?[..8] ?? "null", newCommitSha?[..8] ?? "null");
197 |                     return string.Empty;
198 |                 }
199 | 
200 |                 // Get the changes between the two commits
201 |                 var diffOutput = new StringBuilder();
202 |                 diffOutput.AppendLine($"Changes between {oldCommitSha[..8]} and {newCommitSha[..8]}:");
203 |                 diffOutput.AppendLine();
204 | 
205 |                 // Compare the trees
206 |                 var comparison = repository.Diff.Compare<TreeChanges>(oldCommit.Tree, newCommit.Tree);
207 | 
208 |                 foreach (var change in comparison) {
209 |                     diffOutput.AppendLine($"{change.Status}: {change.Path}");
210 | 
211 |                     // Get detailed patch for each file
212 |                     var patch = repository.Diff.Compare<Patch>(
213 |                         oldCommit.Tree,
214 |                         newCommit.Tree,
215 |                         new[] { change.Path },
216 |                         new CompareOptions { ContextLines = 0 });
217 | 
218 |                     diffOutput.AppendLine(patch);
219 |                 }
220 | 
221 |                 return diffOutput.ToString();
222 |             } catch (Exception ex) {
223 |                 _logger.LogError(ex, "Error getting diff for solution at {SolutionPath}", solutionPath);
224 |                 return $"Error generating diff: {ex.Message}";
225 |             }
226 |         }, cancellationToken);
227 |     }
228 | 
229 |     public async Task<(bool success, string diff)> RevertLastCommitAsync(string solutionPath, CancellationToken cancellationToken = default) {
230 |         return await Task.Run(async () => {
231 |             try {
232 |                 var repositoryPath = GetRepositoryPath(solutionPath);
233 |                 if (repositoryPath == null) {
234 |                     _logger.LogWarning("No Git repository found for solution at {SolutionPath}", solutionPath);
235 |                     return (false, string.Empty);
236 |                 }
237 | 
238 |                 using var repository = new Repository(repositoryPath);
239 |                 var currentBranch = repository.Head.FriendlyName;
240 | 
241 |                 // Ensure we're on a sharptools branch
242 |                 if (!currentBranch.StartsWith(SharpToolsBranchPrefix, StringComparison.OrdinalIgnoreCase)) {
243 |                     _logger.LogWarning("Not on a SharpTools branch, cannot revert. Current branch: {BranchName}", currentBranch);
244 |                     return (false, string.Empty);
245 |                 }
246 | 
247 |                 var currentCommit = repository.Head.Tip;
248 |                 if (currentCommit?.Parents?.Any() != true) {
249 |                     _logger.LogWarning("Current commit has no parent, cannot revert");
250 |                     return (false, string.Empty);
251 |                 }
252 | 
253 |                 var parentCommit = currentCommit.Parents.First();
254 |                 _logger.LogInformation("Reverting from commit {CurrentSha} to parent {ParentSha}",
255 |                     currentCommit.Sha[..8], parentCommit.Sha[..8]);
256 | 
257 |                 // First, create an undo branch at the current commit
258 |                 var undoBranchName = await CreateUndoBranchAsync(solutionPath, cancellationToken);
259 |                 if (string.IsNullOrEmpty(undoBranchName)) {
260 |                     _logger.LogWarning("Failed to create undo branch");
261 |                 }
262 | 
263 |                 // Get the diff before we reset
264 |                 var diff = await GetDiffAsync(solutionPath, parentCommit.Sha, currentCommit.Sha, cancellationToken);
265 | 
266 |                 // Reset to the parent commit (hard reset)
267 |                 repository.Reset(ResetMode.Hard, parentCommit);
268 | 
269 |                 _logger.LogInformation("Successfully reverted to commit {CommitSha}", parentCommit.Sha[..8]);
270 | 
271 |                 var resultMessage = !string.IsNullOrEmpty(undoBranchName)
272 |                     ? $"The changes have been preserved in branch '{undoBranchName}' for future reference."
273 |                     : string.Empty;
274 | 
275 |                 return (true, diff + "\n\n" + resultMessage);
276 |             } catch (Exception ex) {
277 |                 _logger.LogError(ex, "Error reverting last commit for solution at {SolutionPath}", solutionPath);
278 |                 return (false, $"Error: {ex.Message}");
279 |             }
280 |         }, cancellationToken);
281 |     }
282 | 
283 |     public async Task<string> GetBranchOriginCommitAsync(string solutionPath, CancellationToken cancellationToken = default) {
284 |         return await Task.Run(() => {
285 |             try {
286 |                 var repositoryPath = GetRepositoryPath(solutionPath);
287 |                 if (repositoryPath == null) {
288 |                     _logger.LogWarning("No Git repository found for solution at {SolutionPath}", solutionPath);
289 |                     return string.Empty;
290 |                 }
291 | 
292 |                 using var repository = new Repository(repositoryPath);
293 |                 var currentBranch = repository.Head.FriendlyName;
294 | 
295 |                 // Ensure we're on a sharptools branch
296 |                 if (!currentBranch.StartsWith(SharpToolsBranchPrefix, StringComparison.OrdinalIgnoreCase)) {
297 |                     _logger.LogDebug("Not on a SharpTools branch, returning empty. Current branch: {BranchName}", currentBranch);
298 |                     return string.Empty;
299 |                 }
300 | 
301 |                 // Find the commit where this branch diverged from its parent
302 |                 // We'll traverse the commit history to find where the sharptools branch was created
303 |                 var commit = repository.Head.Tip;
304 |                 var branchCreationCommit = commit;
305 | 
306 |                 // Walk back through the commits to find the first commit on this branch
307 |                 while (commit?.Parents?.Any() == true) {
308 |                     var parent = commit.Parents.First();
309 | 
310 |                     // If this is the first commit that mentions sharptools in the branch,
311 |                     // the parent is likely our origin point
312 |                     if (commit.MessageShort.Contains("SharpTools", StringComparison.OrdinalIgnoreCase) ||
313 |                         commit.MessageShort.Contains("branch", StringComparison.OrdinalIgnoreCase)) {
314 |                         branchCreationCommit = parent;
315 |                         break;
316 |                     }
317 | 
318 |                     commit = parent;
319 |                 }
320 | 
321 |                 _logger.LogDebug("Branch origin commit found: {CommitSha}", branchCreationCommit.Sha[..8]);
322 |                 return branchCreationCommit.Sha;
323 |             } catch (Exception ex) {
324 |                 _logger.LogError(ex, "Error finding branch origin commit for solution at {SolutionPath}", solutionPath);
325 |                 return string.Empty;
326 |             }
327 |         }, cancellationToken);
328 |     }
329 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Mcp/ContextInjectors.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System.Text;
  2 | using System.Text.RegularExpressions;
  3 | using DiffPlex.DiffBuilder;
  4 | using Microsoft.CodeAnalysis;
  5 | using Microsoft.CodeAnalysis.Text;
  6 | using Microsoft.Extensions.Logging;
  7 | using SharpTools.Tools.Interfaces;
  8 | using SharpTools.Tools.Mcp.Tools;
  9 | namespace SharpTools.Tools.Mcp;
 10 | /// <summary>
 11 | /// Provides reusable context injection methods for checking compilation errors and generating diffs.
 12 | /// These methods are used across various tools to provide consistent feedback.
 13 | /// </summary>
 14 | internal static class ContextInjectors {
 15 |     /// <summary>
 16 |     /// Checks for compilation errors in a document after code has been modified.
 17 |     /// </summary>
 18 |     /// <param name="solutionManager">The solution manager</param>
 19 |     /// <param name="document">The document to check for errors</param>
 20 |     /// <param name="logger">Logger instance</param>
 21 |     /// <param name="cancellationToken">Cancellation token</param>
 22 |     /// <returns>A tuple containing (hasErrors, errorMessages)</returns>
 23 |     public static async Task<(bool HasErrors, string ErrorMessages)> CheckCompilationErrorsAsync<TLogCategory>(
 24 |     ISolutionManager solutionManager,
 25 |     Document document,
 26 |     ILogger<TLogCategory> logger,
 27 |     CancellationToken cancellationToken) {
 28 |         if (document == null) {
 29 |             logger.LogWarning("Cannot check for compilation errors: Document is null");
 30 |             return (false, string.Empty);
 31 |         }
 32 |         try {
 33 |             // Get the project containing this document
 34 |             var project = document.Project;
 35 |             if (project == null) {
 36 |                 logger.LogWarning("Cannot check for compilation errors: Project not found for document {FilePath}",
 37 |                 document.FilePath ?? "unknown");
 38 |                 return (false, string.Empty);
 39 |             }
 40 |             // Get compilation for the project
 41 |             var compilation = await solutionManager.GetCompilationAsync(project.Id, cancellationToken);
 42 |             if (compilation == null) {
 43 |                 logger.LogWarning("Cannot check for compilation errors: Compilation not available for project {ProjectName}",
 44 |                 project.Name);
 45 |                 return (false, string.Empty);
 46 |             }
 47 |             // Get syntax tree for the document
 48 |             var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
 49 |             if (syntaxTree == null) {
 50 |                 logger.LogWarning("Cannot check for compilation errors: Syntax tree not available for document {FilePath}",
 51 |                 document.FilePath ?? "unknown");
 52 |                 return (false, string.Empty);
 53 |             }
 54 |             // Get semantic model
 55 |             var semanticModel = compilation.GetSemanticModel(syntaxTree);
 56 |             // Get all diagnostics for the specific syntax tree
 57 |             var diagnostics = semanticModel.GetDiagnostics(cancellationToken: cancellationToken)
 58 |             .Where(d => d.Severity == DiagnosticSeverity.Error || d.Severity == DiagnosticSeverity.Warning)
 59 |             .OrderByDescending(d => d.Severity)  // Errors first, then warnings
 60 |             .ThenBy(d => d.Location.SourceSpan.Start)
 61 |             .ToList();
 62 |             if (!diagnostics.Any())
 63 |                 return (false, string.Empty);
 64 |             // Focus specifically on member access errors
 65 |             var memberAccessErrors = diagnostics
 66 |             .Where(d => d.Id == "CS0103" || d.Id == "CS1061" || d.Id == "CS0117" || d.Id == "CS0246")
 67 |             .ToList();
 68 |             // Build error message
 69 |             var sb = new StringBuilder();
 70 |             sb.AppendLine($"<compilationErrors note=\"If the fixes for these errors are simple, use `{ToolHelpers.SharpToolPrefix}{nameof(ModificationTools.FindAndReplace)}`\">");
 71 |             // First add member access errors (highest priority as this is what we're focusing on)
 72 |             foreach (var error in memberAccessErrors) {
 73 |                 var lineSpan = error.Location.GetLineSpan();
 74 |                 sb.AppendLine($"  {error.Severity}: {error.Id} - {error.GetMessage()} at line {lineSpan.StartLinePosition.Line + 1}, column {lineSpan.StartLinePosition.Character + 1}");
 75 |             }
 76 |             // Then add other errors and warnings
 77 |             foreach (var diag in diagnostics.Except(memberAccessErrors)) {
 78 |                 var lineSpan = diag.Location.GetLineSpan();
 79 |                 sb.AppendLine($"  {diag.Severity}: {diag.Id} - {diag.GetMessage()} at line {lineSpan.StartLinePosition.Line + 1}, column {lineSpan.StartLinePosition.Character + 1}");
 80 |             }
 81 |             sb.AppendLine("</compilationErrors>");
 82 |             logger.LogWarning("Compilation issues found in {FilePath}:\n{Errors}",
 83 |             document.FilePath ?? "unknown", sb.ToString());
 84 |             return (true, sb.ToString());
 85 |         } catch (Exception ex) when (!(ex is OperationCanceledException)) {
 86 |             logger.LogError(ex, "Error checking for compilation errors in document {FilePath}",
 87 |             document.FilePath ?? "unknown");
 88 |             return (false, $"Error checking for compilation errors: {ex.Message}");
 89 |         }
 90 |     }
 91 |     /// <summary>
 92 |     /// Creates a pretty diff between old and new code, with whitespace and formatting normalized
 93 |     /// </summary>
 94 |     /// <param name="oldCode">The original code</param>
 95 |     /// <param name="newCode">The updated code</param>
 96 |     /// <param name="includeContextMessage">Whether to include context message about diff being applied</param>
 97 |     /// <returns>Formatted diff as a string</returns>
 98 |     public static string CreateCodeDiff(string oldCode, string newCode) {
 99 |         // Helper function to trim lines for cleaner diff
100 |         static string trimLines(string code) =>
101 |         string.Join("\n", code.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
102 |         .Select(line => line.Trim())
103 |         .Where(line => !string.IsNullOrWhiteSpace(line)));
104 |         string strippedOldCode = trimLines(oldCode);
105 |         string strippedNewCode = trimLines(newCode);
106 |         var diff = InlineDiffBuilder.Diff(strippedOldCode, strippedNewCode);
107 |         var diffBuilder = new StringBuilder();
108 |         bool inUnchangedSection = false;
109 |         foreach (var line in diff.Lines) {
110 |             switch (line.Type) {
111 |                 case DiffPlex.DiffBuilder.Model.ChangeType.Inserted:
112 |                     diffBuilder.AppendLine($"+ {line.Text}");
113 |                     inUnchangedSection = false;
114 |                     break;
115 |                 case DiffPlex.DiffBuilder.Model.ChangeType.Deleted:
116 |                     diffBuilder.AppendLine($"- {line.Text}");
117 |                     inUnchangedSection = false;
118 |                     break;
119 |                 case DiffPlex.DiffBuilder.Model.ChangeType.Unchanged:
120 |                     if (!inUnchangedSection) {
121 |                         diffBuilder.AppendLine("// ...existing code unchanged...");
122 |                         inUnchangedSection = true;
123 |                     }
124 |                     break;
125 |             }
126 |         }
127 |         var diffResult = diffBuilder.ToString();
128 |         if (string.IsNullOrWhiteSpace(diffResult) || diff.Lines.All(l => l.Type == DiffPlex.DiffBuilder.Model.ChangeType.Unchanged)) {
129 |             diffResult = "<diff>\n// No changes detected.\n</diff>";
130 |         } else {
131 |             diffResult = $"<diff>\n{diffResult}\n</diff>\nNote: This diff has been applied. You must base all future changes on the updated code.";
132 |         }
133 |         return diffResult;
134 |     }
135 |     /// <summary>
136 |     /// Creates a diff between old and new document text
137 |     /// </summary>
138 |     /// <param name="oldDocument">The original document</param>
139 |     /// <param name="newDocument">The updated document</param>
140 |     /// <param name="cancellationToken">Cancellation token</param>
141 |     /// <returns>Formatted diff as a string</returns>
142 |     public static async Task<string> CreateDocumentDiff(Document oldDocument, Document newDocument, CancellationToken cancellationToken) {
143 |         if (oldDocument == null || newDocument == null) {
144 |             return "// Could not generate diff: One or both documents are null.";
145 |         }
146 |         var oldText = await oldDocument.GetTextAsync(cancellationToken);
147 |         var newText = await newDocument.GetTextAsync(cancellationToken);
148 |         return CreateCodeDiff(oldText.ToString(), newText.ToString());
149 |     }
150 |     /// <summary>
151 |     /// Creates a multi-document diff for a collection of changed documents
152 |     /// </summary>
153 |     /// <param name="originalSolution">The original solution</param>
154 |     /// <param name="newSolution">The updated solution</param>
155 |     /// <param name="changedDocuments">List of document IDs that were changed</param>
156 |     /// <param name="maxDocuments">Maximum number of documents to include in the diff</param>
157 |     /// <param name="cancellationToken">Cancellation token</param>
158 |     /// <returns>Formatted diff as a string, including file names</returns>
159 |     public static async Task<string> CreateMultiDocumentDiff(
160 |     Solution originalSolution,
161 |     Solution newSolution,
162 |     IReadOnlyList<DocumentId> changedDocuments,
163 |     int maxDocuments = 5,
164 |     CancellationToken cancellationToken = default) {
165 |         if (changedDocuments.Count == 0) {
166 |             return "No documents changed.";
167 |         }
168 |         var sb = new StringBuilder();
169 |         sb.AppendLine($"Changes in {Math.Min(changedDocuments.Count, maxDocuments)} documents:");
170 |         int count = 0;
171 |         foreach (var docId in changedDocuments) {
172 |             if (count >= maxDocuments) {
173 |                 sb.AppendLine($"...and {changedDocuments.Count - maxDocuments} more documents");
174 |                 break;
175 |             }
176 |             var oldDoc = originalSolution.GetDocument(docId);
177 |             var newDoc = newSolution.GetDocument(docId);
178 |             if (oldDoc == null || newDoc == null) {
179 |                 continue;
180 |             }
181 |             sb.AppendLine();
182 |             sb.AppendLine($"Document: {oldDoc.FilePath}");
183 |             sb.AppendLine(await CreateDocumentDiff(oldDoc, newDoc, cancellationToken));
184 |             count++;
185 |         }
186 |         return sb.ToString();
187 |     }
188 |     public static async Task<string> CreateCallGraphContextAsync<TLogCategory>(
189 |         ICodeAnalysisService codeAnalysisService,
190 |         ILogger<TLogCategory> logger,
191 |         IMethodSymbol methodSymbol,
192 |         CancellationToken cancellationToken) {
193 |         if (methodSymbol == null) {
194 |             return "Method symbol is null.";
195 |         }
196 | 
197 |         var callers = new HashSet<string>();
198 |         var callees = new HashSet<string>();
199 | 
200 |         try {
201 |             // Get incoming calls (callers)
202 |             var callerInfos = await codeAnalysisService.FindCallersAsync(methodSymbol, cancellationToken);
203 |             foreach (var callerInfo in callerInfos) {
204 |                 cancellationToken.ThrowIfCancellationRequested();
205 |                 if (callerInfo.CallingSymbol is IMethodSymbol callingMethodSymbol) {
206 |                     // We still show all callers, since this is important for analysis
207 |                     string callerFqn = FuzzyFqnLookupService.GetSearchableString(callingMethodSymbol);
208 |                     callers.Add(callerFqn);
209 |                 }
210 |             }
211 | 
212 |             // Get outgoing calls (callees)
213 |             var outgoingSymbols = await codeAnalysisService.FindOutgoingCallsAsync(methodSymbol, cancellationToken);
214 |             foreach (var callee in outgoingSymbols) {
215 |                 cancellationToken.ThrowIfCancellationRequested();
216 |                 if (callee is IMethodSymbol calleeMethodSymbol) {
217 |                     // Only include callees that are defined within the solution
218 |                     if (IsSymbolInSolution(calleeMethodSymbol)) {
219 |                         string calleeFqn = FuzzyFqnLookupService.GetSearchableString(calleeMethodSymbol);
220 |                         callees.Add(calleeFqn);
221 |                     }
222 |                 }
223 |             }
224 |         } catch (Exception ex) when (!(ex is OperationCanceledException)) {
225 |             logger.LogWarning(ex, "Error creating call graph context for method {MethodName}", methodSymbol.Name);
226 |             return $"Error creating call graph: {ex.Message}";
227 |         }
228 | 
229 |         // Format results in XML format
230 |         var random = new Random();
231 |         var result = new StringBuilder();
232 |         result.AppendLine("<callers>");
233 |         var randomizedCallers = callers.OrderBy(_ => random.Next()).Take(20);
234 |         foreach (var caller in randomizedCallers) {
235 |             result.AppendLine(caller);
236 |         }
237 |         if (callers.Count > 20) {
238 |             result.AppendLine($"<!-- {callers.Count - 20} more callers not shown -->");
239 |         }
240 |         result.AppendLine("</callers>");
241 |         result.AppendLine("<callees>");
242 |         foreach (var callee in callees.OrderBy(_ => random.Next()).Take(20)) {
243 |             result.AppendLine(callee);
244 |         }
245 |         if (callees.Count > 20) {
246 |             result.AppendLine($"<!-- {callees.Count - 20} more callees not shown -->");
247 |         }
248 |         result.AppendLine("</callees>");
249 | 
250 |         return result.ToString();
251 |     }
252 |     public static async Task<string> CreateTypeReferenceContextAsync<TLogCategory>(
253 |         ICodeAnalysisService codeAnalysisService,
254 |         ILogger<TLogCategory> logger,
255 |         INamedTypeSymbol typeSymbol,
256 |         CancellationToken cancellationToken) {
257 |         if (typeSymbol == null) {
258 |             return "Type symbol is null.";
259 |         }
260 | 
261 |         var referencingTypes = new HashSet<string>();
262 |         var referencedTypes = new HashSet<string>(StringComparer.Ordinal);
263 | 
264 |         try {
265 |             // Get referencing types (types that reference this type)
266 |             var references = await codeAnalysisService.FindReferencesAsync(typeSymbol, cancellationToken);
267 |             foreach (var reference in references) {
268 |                 cancellationToken.ThrowIfCancellationRequested();
269 |                 foreach (var location in reference.Locations) {
270 |                     if (location.Document == null || location.Location == null) {
271 |                         continue;
272 |                     }
273 | 
274 |                     var semanticModel = await location.Document.GetSemanticModelAsync(cancellationToken);
275 |                     if (semanticModel == null) {
276 |                         continue;
277 |                     }
278 | 
279 |                     var symbol = semanticModel.GetEnclosingSymbol(location.Location.SourceSpan.Start, cancellationToken);
280 |                     while (symbol != null && !(symbol is INamedTypeSymbol)) {
281 |                         symbol = symbol.ContainingSymbol;
282 |                     }
283 | 
284 |                     if (symbol is INamedTypeSymbol referencingType &&
285 |                         !SymbolEqualityComparer.Default.Equals(referencingType, typeSymbol)) {
286 |                         // We still include all referencing types, since this is important for analysis
287 |                         string referencingTypeFqn = FuzzyFqnLookupService.GetSearchableString(referencingType);
288 |                         referencingTypes.Add(referencingTypeFqn);
289 |                     }
290 |                 }
291 |             }
292 | 
293 |             // Get referenced types (types this type references in implementations)
294 |             // This was moved to CodeAnalysisService.FindReferencedTypesAsync
295 |             referencedTypes = await codeAnalysisService.FindReferencedTypesAsync(typeSymbol, cancellationToken);
296 |         } catch (Exception ex) when (!(ex is OperationCanceledException)) {
297 |             logger.LogWarning(ex, "Error creating type reference context for type {TypeName}", typeSymbol.Name);
298 |             return $"Error creating type reference context: {ex.Message}";
299 |         }
300 | 
301 |         // Format results in XML format
302 |         var random = new Random();
303 |         var result = new StringBuilder();
304 |         result.AppendLine("<referencingTypes>");
305 |         foreach (var referencingType in referencingTypes.OrderBy(t => random.Next()).Take(20)) {
306 |             result.AppendLine(referencingType);
307 |         }
308 |         if (referencingTypes.Count > 20) {
309 |             result.AppendLine($"<!-- {referencingTypes.Count - 20} more referencing types not shown -->");
310 |         }
311 |         result.AppendLine("</referencingTypes>");
312 |         result.AppendLine("<referencedTypes>");
313 |         foreach (var referencedType in referencedTypes.OrderBy(t => random.Next()).Take(20)) {
314 |             result.AppendLine(referencedType);
315 |         }
316 |         if (referencedTypes.Count > 20) {
317 |             result.AppendLine($"<!-- {referencedTypes.Count - 20} more referenced types not shown -->");
318 |         }
319 |         result.AppendLine("</referencedTypes>");
320 | 
321 |         return result.ToString();
322 |     }/// <summary>
323 |      /// Determines if a symbol is defined within the current solution.
324 |      /// </summary>
325 |      /// <param name="symbol">The symbol to check</param>
326 |      /// <returns>True if the symbol is defined within the solution, false otherwise</returns>
327 |     private static bool IsSymbolInSolution(ISymbol symbol) {
328 |         if (symbol == null) {
329 |             return false;
330 |         }
331 | 
332 |         // Get the containing assembly of the symbol
333 |         var assembly = symbol.ContainingAssembly;
334 |         if (assembly == null) {
335 |             return false;
336 |         }
337 | 
338 |         // Check if the assembly is from source code (part of the solution)
339 |         // Assemblies in the solution have source code locations, while referenced assemblies don't
340 |         return assembly.Locations.Any(loc => loc.IsInSource);
341 |     }
342 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/ComplexityAnalysisService.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Microsoft.CodeAnalysis;
  2 | using Microsoft.CodeAnalysis.CSharp;
  3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
  4 | using Microsoft.Extensions.Logging;
  5 | using System;
  6 | using System.Collections.Generic;
  7 | using System.Linq;
  8 | using System.Threading;
  9 | using System.Threading.Tasks;
 10 | using SharpTools.Tools.Extensions;
 11 | using SharpTools.Tools.Services;
 12 | using ModelContextProtocol;
 13 | 
 14 | namespace SharpTools.Tools.Services;
 15 | 
 16 | /// <summary>
 17 | /// Service for analyzing code complexity metrics.
 18 | /// </summary>
 19 | public class ComplexityAnalysisService : IComplexityAnalysisService {
 20 |     private readonly ISolutionManager _solutionManager;
 21 |     private readonly ILogger<ComplexityAnalysisService> _logger;
 22 | 
 23 |     public ComplexityAnalysisService(ISolutionManager solutionManager, ILogger<ComplexityAnalysisService> logger) {
 24 |         _solutionManager = solutionManager;
 25 |         _logger = logger;
 26 |     }
 27 |     public async Task AnalyzeMethodAsync(
 28 |         IMethodSymbol methodSymbol,
 29 |         Dictionary<string, object> metrics,
 30 |         List<string> recommendations,
 31 |         CancellationToken cancellationToken) {
 32 |         var syntaxRef = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault();
 33 |         if (syntaxRef == null) {
 34 |             _logger.LogWarning("Method {Method} has no syntax reference", methodSymbol.Name);
 35 |             return;
 36 |         }
 37 | 
 38 |         var methodNode = await syntaxRef.GetSyntaxAsync(cancellationToken) as MethodDeclarationSyntax;
 39 |         if (methodNode == null) {
 40 |             _logger.LogWarning("Could not get method syntax for {Method}", methodSymbol.Name);
 41 |             return;
 42 |         }
 43 | 
 44 |         // Basic metrics
 45 |         var lineCount = methodNode.GetText().Lines.Count;
 46 |         var statementCount = methodNode.DescendantNodes().OfType<StatementSyntax>().Count();
 47 |         var parameterCount = methodSymbol.Parameters.Length;
 48 |         var localVarCount = methodNode.DescendantNodes().OfType<LocalDeclarationStatementSyntax>().Count();
 49 | 
 50 |         metrics["lineCount"] = lineCount;
 51 |         metrics["statementCount"] = statementCount;
 52 |         metrics["parameterCount"] = parameterCount;
 53 |         metrics["localVariableCount"] = localVarCount;
 54 | 
 55 |         // Cyclomatic complexity
 56 |         int cyclomaticComplexity = 1; // Base complexity
 57 |         cyclomaticComplexity += methodNode.DescendantNodes().Count(n => {
 58 |             switch (n) {
 59 |                 case IfStatementSyntax:
 60 |                 case SwitchSectionSyntax:
 61 |                 case ForStatementSyntax:
 62 |                 case ForEachStatementSyntax:
 63 |                 case WhileStatementSyntax:
 64 |                 case DoStatementSyntax:
 65 |                 case CatchClauseSyntax:
 66 |                 case ConditionalExpressionSyntax:
 67 |                     return true;
 68 |                 case BinaryExpressionSyntax bex:
 69 |                     return bex.IsKind(SyntaxKind.LogicalAndExpression) ||
 70 |                            bex.IsKind(SyntaxKind.LogicalOrExpression);
 71 |                 default:
 72 |                     return false;
 73 |             }
 74 |         });
 75 | 
 76 |         metrics["cyclomaticComplexity"] = cyclomaticComplexity;
 77 | 
 78 |         // Cognitive complexity (simplified version)
 79 |         int cognitiveComplexity = 0;
 80 |         int nesting = 0;
 81 | 
 82 |         void AddCognitiveComplexity(int value) => cognitiveComplexity += value + nesting;
 83 | 
 84 |         foreach (var node in methodNode.DescendantNodes()) {
 85 |             bool isNestingNode = false;
 86 | 
 87 |             switch (node) {
 88 |                 case IfStatementSyntax:
 89 |                 case ForStatementSyntax:
 90 |                 case ForEachStatementSyntax:
 91 |                 case WhileStatementSyntax:
 92 |                 case DoStatementSyntax:
 93 |                 case CatchClauseSyntax:
 94 |                     AddCognitiveComplexity(1);
 95 |                     isNestingNode = true;
 96 |                     break;
 97 |                 case SwitchStatementSyntax:
 98 |                     AddCognitiveComplexity(1);
 99 |                     break;
100 |                 case BinaryExpressionSyntax bex:
101 |                     if (bex.IsKind(SyntaxKind.LogicalAndExpression) ||
102 |                         bex.IsKind(SyntaxKind.LogicalOrExpression)) {
103 |                         AddCognitiveComplexity(1);
104 |                     }
105 |                     break;
106 |                 case LambdaExpressionSyntax:
107 |                     AddCognitiveComplexity(1);
108 |                     isNestingNode = true;
109 |                     break;
110 |                 case RecursivePatternSyntax:
111 |                     AddCognitiveComplexity(1);
112 |                     break;
113 |             }
114 | 
115 |             if (isNestingNode) {
116 |                 nesting++;
117 |                 // We'll decrement nesting when processing the block end
118 |             }
119 |         }
120 | 
121 |         metrics["cognitiveComplexity"] = cognitiveComplexity;
122 | 
123 |         // Outgoing dependencies (method calls)
124 |         // Check if solution is available before using it
125 |         int methodCallCount = 0;
126 |         if (_solutionManager.CurrentSolution != null) {
127 |             var compilation = await _solutionManager.GetCompilationAsync(
128 |                 methodNode.SyntaxTree.GetRequiredProject(_solutionManager.CurrentSolution).Id,
129 |                 cancellationToken);
130 | 
131 |             if (compilation != null) {
132 |                 var semanticModel = compilation.GetSemanticModel(methodNode.SyntaxTree);
133 |                 var methodCalls = methodNode.DescendantNodes()
134 |                     .OfType<InvocationExpressionSyntax>()
135 |                     .Select(i => semanticModel.GetSymbolInfo(i).Symbol)
136 |                     .OfType<IMethodSymbol>()
137 |                     .Where(m => !SymbolEqualityComparer.Default.Equals(m.ContainingType, methodSymbol.ContainingType))
138 |                     .Select(m => m.ContainingType.ToDisplayString())
139 |                     .Distinct()
140 |                     .ToList();
141 |                 methodCallCount = methodCalls.Count;
142 |                 metrics["externalMethodCalls"] = methodCallCount;
143 |                 metrics["externalDependencies"] = methodCalls;
144 |             }
145 |         } else {
146 |             _logger.LogWarning("Cannot analyze method dependencies: No solution loaded");
147 |         }
148 | 
149 |         // Add recommendations based on metrics
150 |         if (lineCount > 50)
151 |             recommendations.Add($"Method '{methodSymbol.Name}' is {lineCount} lines long. Consider breaking it into smaller methods.");
152 | 
153 |         if (cyclomaticComplexity > 10)
154 |             recommendations.Add($"Method '{methodSymbol.Name}' has high cyclomatic complexity ({cyclomaticComplexity}). Consider refactoring into smaller methods.");
155 | 
156 |         if (cognitiveComplexity > 20)
157 |             recommendations.Add($"Method '{methodSymbol.Name}' has high cognitive complexity ({cognitiveComplexity}). Consider simplifying the logic or breaking it down.");
158 | 
159 |         if (parameterCount > 4)
160 |             recommendations.Add($"Method '{methodSymbol.Name}' has {parameterCount} parameters. Consider grouping related parameters into a class.");
161 | 
162 |         if (localVarCount > 10)
163 |             recommendations.Add($"Method '{methodSymbol.Name}' has {localVarCount} local variables. Consider breaking some logic into helper methods.");
164 | 
165 |         if (methodCallCount > 5)
166 |             recommendations.Add($"Method '{methodSymbol.Name}' has {methodCallCount} external method calls. Consider reducing dependencies or breaking it into smaller methods.");
167 |     }
168 |     public async Task AnalyzeTypeAsync(
169 |         INamedTypeSymbol typeSymbol,
170 |         Dictionary<string, object> metrics,
171 |         List<string> recommendations,
172 |         bool includeGeneratedCode,
173 |         CancellationToken cancellationToken) {
174 |         var typeMetrics = new Dictionary<string, object>();
175 | 
176 |         // Basic type metrics
177 |         typeMetrics["kind"] = typeSymbol.TypeKind.ToString();
178 |         typeMetrics["isAbstract"] = typeSymbol.IsAbstract;
179 |         typeMetrics["isSealed"] = typeSymbol.IsSealed;
180 |         typeMetrics["isGeneric"] = typeSymbol.IsGenericType;
181 | 
182 |         // Member counts
183 |         var members = typeSymbol.GetMembers();
184 |         typeMetrics["totalMemberCount"] = members.Length;
185 |         typeMetrics["methodCount"] = members.Count(m => m is IMethodSymbol);
186 |         typeMetrics["propertyCount"] = members.Count(m => m is IPropertySymbol);
187 |         typeMetrics["fieldCount"] = members.Count(m => m is IFieldSymbol);
188 |         typeMetrics["eventCount"] = members.Count(m => m is IEventSymbol);
189 | 
190 |         // Inheritance metrics
191 |         var baseTypes = new List<string>();
192 |         var inheritanceDepth = 0;
193 |         var currentType = typeSymbol.BaseType;
194 | 
195 |         while (currentType != null && !currentType.SpecialType.Equals(SpecialType.System_Object)) {
196 |             baseTypes.Add(currentType.ToDisplayString());
197 |             inheritanceDepth++;
198 |             currentType = currentType.BaseType;
199 |         }
200 | 
201 |         typeMetrics["inheritanceDepth"] = inheritanceDepth;
202 |         typeMetrics["baseTypes"] = baseTypes;
203 |         typeMetrics["implementedInterfaces"] = typeSymbol.AllInterfaces.Select(i => i.ToDisplayString()).ToList();
204 | 
205 |         // Analyze methods
206 |         var methodMetrics = new List<Dictionary<string, object>>();
207 |         var methodComplexitySum = 0;
208 |         var methodCount = 0;
209 | 
210 |         foreach (var member in members.OfType<IMethodSymbol>()) {
211 |             if (member.IsImplicitlyDeclared) continue;
212 | 
213 |             var methodDict = new Dictionary<string, object>();
214 |             await AnalyzeMethodAsync(member, methodDict, recommendations, cancellationToken);
215 | 
216 |             if (methodDict.ContainsKey("cyclomaticComplexity")) {
217 |                 methodComplexitySum += (int)methodDict["cyclomaticComplexity"];
218 |                 methodCount++;
219 |             }
220 | 
221 |             methodMetrics.Add(methodDict);
222 |         }
223 | 
224 |         typeMetrics["methods"] = methodMetrics;
225 |         typeMetrics["averageMethodComplexity"] = methodCount > 0 ? (double)methodComplexitySum / methodCount : 0;
226 | 
227 |         // Coupling analysis
228 |         var dependencies = new HashSet<string>();
229 |         var syntaxRefs = typeSymbol.DeclaringSyntaxReferences;
230 | 
231 |         // Check if solution is available before using it
232 |         if (_solutionManager.CurrentSolution != null) {
233 |             foreach (var syntaxRef in syntaxRefs) {
234 |                 var syntax = await syntaxRef.GetSyntaxAsync(cancellationToken);
235 |                 var project = syntax.SyntaxTree.GetRequiredProject(_solutionManager.CurrentSolution);
236 |                 var compilation = await _solutionManager.GetCompilationAsync(project.Id, cancellationToken);
237 | 
238 |                 if (compilation != null) {
239 |                     var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
240 | 
241 |                     // Find all type references in the class
242 |                     foreach (var node in syntax.DescendantNodes()) {
243 |                         if (cancellationToken.IsCancellationRequested) break; var symbolInfo = semanticModel.GetSymbolInfo(node).Symbol;
244 |                         if (symbolInfo?.ContainingType != null &&
245 |                         !SymbolEqualityComparer.Default.Equals(symbolInfo.ContainingType, typeSymbol) &&
246 |                         !symbolInfo.ContainingType.SpecialType.Equals(SpecialType.System_Object)) {
247 |                             dependencies.Add(symbolInfo.ContainingType.ToDisplayString());
248 |                         }
249 |                     }
250 |                 }
251 |             }
252 |         } else {
253 |             _logger.LogWarning("Cannot analyze type dependencies: No solution loaded");
254 |         }
255 | 
256 |         typeMetrics["dependencyCount"] = dependencies.Count;
257 |         typeMetrics["dependencies"] = dependencies.ToList();
258 | 
259 |         // Add type-level recommendations
260 |         if (inheritanceDepth > 5)
261 |             recommendations.Add($"Type '{typeSymbol.Name}' has deep inheritance ({inheritanceDepth} levels). Consider composition over inheritance.");
262 | 
263 |         if (dependencies.Count > 20)
264 |             recommendations.Add($"Type '{typeSymbol.Name}' has high coupling ({dependencies.Count} dependencies). Consider breaking it into smaller classes.");
265 | 
266 |         if (members.Length > 50)
267 |             recommendations.Add($"Type '{typeSymbol.Name}' has {members.Length} members. Consider breaking it into smaller, focused classes.");
268 | 
269 |         if (typeMetrics["averageMethodComplexity"] is double avg && avg > 12)
270 |             recommendations.Add($"Type '{typeSymbol.Name}' has high average method complexity ({avg:F1}). Consider refactoring complex methods.");
271 | 
272 |         metrics["typeMetrics"] = typeMetrics;
273 |     }
274 |     public async Task AnalyzeProjectAsync(
275 |         Project project,
276 |         Dictionary<string, object> metrics,
277 |         List<string> recommendations,
278 |         bool includeGeneratedCode,
279 |         CancellationToken cancellationToken) {
280 |         var projectMetrics = new Dictionary<string, object>();
281 |         var typeMetrics = new List<Dictionary<string, object>>();
282 | 
283 |         // Project-wide metrics
284 |         var compilation = await project.GetCompilationAsync(cancellationToken);
285 |         if (compilation == null) {
286 |             throw new McpException($"Could not get compilation for project {project.Name}");
287 |         }
288 | 
289 |         var syntaxTrees = compilation.SyntaxTrees;
290 |         if (!includeGeneratedCode) {
291 |             syntaxTrees = syntaxTrees.Where(tree =>
292 |                 !tree.FilePath.Contains(".g.cs") &&
293 |                 !tree.FilePath.Contains(".Designer.cs"));
294 |         }
295 | 
296 |         projectMetrics["fileCount"] = syntaxTrees.Count();
297 | 
298 |         // Calculate total lines manually to avoid async enumeration complexity
299 |         var totalLines = 0;
300 |         foreach (var tree in syntaxTrees) {
301 |             if (cancellationToken.IsCancellationRequested) break;
302 |             var text = await tree.GetTextAsync(cancellationToken);
303 |             totalLines += text.Lines.Count;
304 |         }
305 |         projectMetrics["totalLines"] = totalLines;
306 | 
307 |         var globalComplexityMetrics = new Dictionary<string, object> {
308 |             ["totalCyclomaticComplexity"] = 0,
309 |             ["totalCognitiveComplexity"] = 0,
310 |             ["maxMethodComplexity"] = 0,
311 |             ["complexMethodCount"] = 0,
312 |             ["averageMethodComplexity"] = 0.0,
313 |             ["methodCount"] = 0
314 |         };
315 | 
316 |         foreach (var tree in syntaxTrees) {
317 |             if (cancellationToken.IsCancellationRequested) break;
318 | 
319 |             var semanticModel = compilation.GetSemanticModel(tree);
320 |             var root = await tree.GetRootAsync(cancellationToken);
321 | 
322 |             // Analyze each type in the file
323 |             foreach (var typeDecl in root.DescendantNodes().OfType<TypeDeclarationSyntax>()) {
324 |                 var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl) as INamedTypeSymbol;
325 |                 if (typeSymbol != null) {
326 |                     var typeDict = new Dictionary<string, object>();
327 |                     await AnalyzeTypeAsync(typeSymbol, typeDict, recommendations, includeGeneratedCode, cancellationToken);
328 |                     typeMetrics.Add(typeDict);
329 | 
330 |                     // Aggregate complexity metrics
331 |                     if (typeDict.TryGetValue("typeMetrics", out var typeMetricsObj) &&
332 |                         typeMetricsObj is Dictionary<string, object> tm &&
333 |                         tm.TryGetValue("methods", out var methodsObj) &&
334 |                         methodsObj is List<Dictionary<string, object>> methods) {
335 |                         foreach (var method in methods) {
336 |                             if (method.TryGetValue("cyclomaticComplexity", out var ccObj) &&
337 |                                 ccObj is int cc) {
338 |                                 globalComplexityMetrics["totalCyclomaticComplexity"] =
339 |                                     (int)globalComplexityMetrics["totalCyclomaticComplexity"] + cc;
340 | 
341 |                                 globalComplexityMetrics["maxMethodComplexity"] =
342 |                                     Math.Max((int)globalComplexityMetrics["maxMethodComplexity"], cc);
343 | 
344 |                                 if (cc > 10)
345 |                                     globalComplexityMetrics["complexMethodCount"] =
346 |                                         (int)globalComplexityMetrics["complexMethodCount"] + 1;
347 | 
348 |                                 globalComplexityMetrics["methodCount"] =
349 |                                     (int)globalComplexityMetrics["methodCount"] + 1;
350 |                             }
351 | 
352 |                             if (method.TryGetValue("cognitiveComplexity", out var cogObj) &&
353 |                                 cogObj is int cog) {
354 |                                 globalComplexityMetrics["totalCognitiveComplexity"] =
355 |                                     (int)globalComplexityMetrics["totalCognitiveComplexity"] + cog;
356 |                             }
357 |                         }
358 |                     }
359 |                 }
360 |             }
361 |         }
362 | 
363 |         // Calculate averages
364 |         if ((int)globalComplexityMetrics["methodCount"] > 0) {
365 |             globalComplexityMetrics["averageMethodComplexity"] =
366 |                 (double)(int)globalComplexityMetrics["totalCyclomaticComplexity"] /
367 |                 (int)globalComplexityMetrics["methodCount"];
368 |         }
369 | 
370 |         projectMetrics["complexityMetrics"] = globalComplexityMetrics;
371 |         projectMetrics["typeMetrics"] = typeMetrics;
372 | 
373 |         // Project-wide recommendations
374 |         var avgComplexity = (double)globalComplexityMetrics["averageMethodComplexity"];
375 |         var complexMethodCount = (int)globalComplexityMetrics["complexMethodCount"];
376 | 
377 |         if (avgComplexity > 5)
378 |             recommendations.Add($"Project has high average method complexity ({avgComplexity:F1}). Consider refactoring complex methods.");
379 | 
380 |         if (complexMethodCount > 0)
381 |             recommendations.Add($"Project has {complexMethodCount} methods with high cyclomatic complexity (>10). Consider refactoring these methods.");
382 | 
383 |         var totalTypes = typeMetrics.Count;
384 |         if (totalTypes > 50)
385 |             recommendations.Add($"Project has {totalTypes} types. Consider breaking it into multiple projects if they serve different concerns.");
386 | 
387 |         metrics["projectMetrics"] = projectMetrics;
388 |     }
389 | }
```

--------------------------------------------------------------------------------
/SharpTools.Tools/Services/DocumentOperationsService.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Linq;
  5 | using System.Threading;
  6 | using System.Threading.Tasks;
  7 | using System.Xml;
  8 | using Microsoft.CodeAnalysis.Text;
  9 | 
 10 | namespace SharpTools.Tools.Services;
 11 | 
 12 | public class DocumentOperationsService : IDocumentOperationsService {
 13 |     private readonly ISolutionManager _solutionManager;
 14 |     private readonly ICodeModificationService _modificationService;
 15 |     private readonly IGitService _gitService;
 16 |     private readonly ILogger<DocumentOperationsService> _logger;
 17 | 
 18 |     // Extensions for common code file types that can be formatted
 19 |     private static readonly HashSet<string> CodeFileExtensions = new(StringComparer.OrdinalIgnoreCase) {
 20 |         ".cs", ".csproj", ".sln", ".css", ".js", ".ts", ".jsx", ".tsx", ".html", ".cshtml", ".razor", ".yml", ".yaml",
 21 |         ".json", ".xml", ".config", ".md", ".fs", ".fsx", ".fsi", ".vb"
 22 |     };
 23 | 
 24 |     private static readonly HashSet<string> UnsafeDirectories = new(StringComparer.OrdinalIgnoreCase) {
 25 |         ".git", ".vs", "bin", "obj", "node_modules"
 26 |     };
 27 | 
 28 |     public DocumentOperationsService(
 29 |         ISolutionManager solutionManager,
 30 |         ICodeModificationService modificationService,
 31 |         IGitService gitService,
 32 |         ILogger<DocumentOperationsService> logger) {
 33 |         _solutionManager = solutionManager;
 34 |         _modificationService = modificationService;
 35 |         _gitService = gitService;
 36 |         _logger = logger;
 37 |     }
 38 | 
 39 |     public async Task<(string contents, int lines)> ReadFileAsync(string filePath, bool omitLeadingSpaces, CancellationToken cancellationToken) {
 40 |         if (!File.Exists(filePath)) {
 41 |             throw new FileNotFoundException($"File not found: {filePath}");
 42 |         }
 43 | 
 44 |         if (!IsPathReadable(filePath)) {
 45 |             throw new UnauthorizedAccessException($"Reading from this path is not allowed: {filePath}");
 46 |         }
 47 | 
 48 |         string content = await File.ReadAllTextAsync(filePath, cancellationToken);
 49 |         var lines = content.Split(["\r\n", "\r", "\n"], StringSplitOptions.None);
 50 | 
 51 |         if (omitLeadingSpaces) {
 52 | 
 53 |             for (int i = 0; i < lines.Length; i++) {
 54 |                 lines[i] = TrimLeadingSpaces(lines[i]);
 55 |             }
 56 | 
 57 |             content = string.Join(Environment.NewLine, lines);
 58 |         }
 59 | 
 60 |         return (content, lines.Length);
 61 |     }
 62 |     public async Task<bool> WriteFileAsync(string filePath, string content, bool overwriteIfExists, CancellationToken cancellationToken, string commitMessage) {
 63 |         var pathInfo = GetPathInfo(filePath);
 64 | 
 65 |         if (!pathInfo.IsWritable) {
 66 |             _logger.LogWarning("Path is not writable: {FilePath}. Reason: {Reason}",
 67 |                 filePath, pathInfo.WriteRestrictionReason);
 68 |             throw new UnauthorizedAccessException($"Writing to this path is not allowed: {filePath}. {pathInfo.WriteRestrictionReason}");
 69 |         }
 70 | 
 71 |         if (File.Exists(filePath) && !overwriteIfExists) {
 72 |             _logger.LogWarning("File already exists and overwrite not allowed: {FilePath}", filePath);
 73 |             return false;
 74 |         }
 75 | 
 76 |         // Ensure directory exists
 77 |         string? directory = Path.GetDirectoryName(filePath);
 78 |         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) {
 79 |             Directory.CreateDirectory(directory);
 80 |         }
 81 | 
 82 |         // Write the content to the file
 83 |         await File.WriteAllTextAsync(filePath, content, cancellationToken);
 84 |         _logger.LogInformation("File {Operation} at {FilePath}",
 85 |             File.Exists(filePath) ? "overwritten" : "created", filePath);
 86 | 
 87 | 
 88 |         // Find the most appropriate project for this file path
 89 |         var bestProject = FindMostAppropriateProject(filePath);
 90 |         if (!pathInfo.IsFormattable || bestProject is null || string.IsNullOrWhiteSpace(bestProject.FilePath)) {
 91 |             _logger.LogWarning("Added non-code file: {FilePath}", filePath);
 92 |             if (string.IsNullOrEmpty(commitMessage)) {
 93 |                 return true; // No commit message provided, don't commit, just return
 94 |             }
 95 |             //just commit the file
 96 |             await ProcessGitOperationsAsync([filePath], cancellationToken, commitMessage);
 97 |             return true;
 98 |         }
 99 | 
100 |         Project? legacyProject = null;
101 |         bool isSdkStyleProject = await IsSDKStyleProjectAsync(bestProject.FilePath, cancellationToken);
102 |         if (isSdkStyleProject) {
103 |             _logger.LogInformation("File added to SDK-style project: {ProjectPath}. Reloading Solution to pick up changes.", bestProject.FilePath);
104 |             await _solutionManager.ReloadSolutionFromDiskAsync(cancellationToken);
105 |         } else {
106 |             legacyProject = await TryAddFileToLegacyProjectAsync(filePath, bestProject, cancellationToken);
107 |         }
108 |         var newSolution = legacyProject?.Solution ?? _solutionManager.CurrentSolution;
109 |         var documentId = newSolution?.GetDocumentIdsWithFilePath(filePath).FirstOrDefault();
110 |         if (documentId is null) {
111 |             _logger.LogWarning("Mystery file was not added to any project: {FilePath}", filePath);
112 |             return false;
113 |         }
114 |         var document = newSolution?.GetDocument(documentId);
115 |         if (document is null) {
116 |             _logger.LogWarning("Document not found in solution: {FilePath}", filePath);
117 |             return false;
118 |         }
119 |         // If it's a code file, try to format it, which will also commit it
120 |         if (await TryFormatAndCommitFileAsync(document, cancellationToken, commitMessage)) {
121 |             _logger.LogInformation("File formatted and committed: {FilePath}", filePath);
122 |             return true;
123 |         } else {
124 |             _logger.LogWarning("Failed to format file: {FilePath}", filePath);
125 |         }
126 |         return true;
127 |     }
128 | 
129 |     private async Task<Project?> TryAddFileToLegacyProjectAsync(string filePath, Project project, CancellationToken cancellationToken) {
130 |         if (!_solutionManager.IsSolutionLoaded || !File.Exists(filePath)) {
131 |             return null;
132 |         }
133 | 
134 |         try {
135 |             // Get the document ID if the file is already in the solution
136 |             var documentId = _solutionManager.CurrentSolution!.GetDocumentIdsWithFilePath(filePath).FirstOrDefault();
137 | 
138 |             // If the document is already in the solution, no need to add it again
139 |             if (documentId != null) {
140 |                 _logger.LogInformation("File is already part of project: {FilePath}", filePath);
141 |                 return null;
142 |             }
143 | 
144 |             // The file exists on disk but is not part of the project yet - add it to the solution in memory
145 |             var fileName = Path.GetFileName(filePath);
146 | 
147 |             // Determine appropriate folder path relative to the project
148 |             var projectDir = Path.GetDirectoryName(project.FilePath);
149 |             var relativePath = string.Empty;
150 |             var folders = Array.Empty<string>();
151 | 
152 |             if (!string.IsNullOrEmpty(projectDir)) {
153 |                 relativePath = Path.GetRelativePath(projectDir, filePath);
154 |                 var folderPath = Path.GetDirectoryName(relativePath);
155 | 
156 |                 if (!string.IsNullOrEmpty(folderPath) && folderPath != ".") {
157 |                     folders = folderPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
158 |                 }
159 |             }
160 | 
161 |             _logger.LogInformation("Adding file to {ProjectName}: {FilePath}", project.Name, filePath);
162 | 
163 |             // Create SourceText from file content
164 |             var fileContent = await File.ReadAllTextAsync(filePath, cancellationToken);
165 |             var sourceText = SourceText.From(fileContent);
166 | 
167 |             // Add the document to the project in memory
168 |             return project.AddDocument(fileName, sourceText, folders, filePath).Project;
169 |         } catch (Exception ex) {
170 |             _logger.LogError(ex, "Failed to add file {FilePath} to project", filePath);
171 |             return null;
172 |         }
173 |     }
174 | 
175 |     private async Task<bool> IsSDKStyleProjectAsync(string projectFilePath, CancellationToken cancellationToken) {
176 |         try {
177 |             var content = await File.ReadAllTextAsync(projectFilePath, cancellationToken);
178 | 
179 |             // Use XmlDocument for proper parsing
180 |             var xmlDoc = new XmlDocument();
181 |             xmlDoc.LoadXml(content);
182 | 
183 |             var projectNode = xmlDoc.DocumentElement;
184 | 
185 |             // Primary check - Look for Sdk attribute on Project element
186 |             if (projectNode?.Attributes?["Sdk"] != null) {
187 |                 _logger.LogDebug("Project {ProjectPath} is SDK-style (has Sdk attribute)", projectFilePath);
188 |                 return true;
189 |             }
190 | 
191 |             // Secondary check - Look for TargetFramework instead of TargetFrameworkVersion
192 |             var targetFrameworkNode = xmlDoc.SelectSingleNode("//TargetFramework");
193 |             if (targetFrameworkNode != null) {
194 |                 _logger.LogDebug("Project {ProjectPath} is SDK-style (uses TargetFramework)", projectFilePath);
195 |                 return true;
196 |             }
197 | 
198 |             _logger.LogDebug("Project {ProjectPath} is classic-style (no SDK indicators found)", projectFilePath);
199 |             return false;
200 |         } catch (Exception ex) {
201 |             _logger.LogWarning(ex, "Error determining project style for {ProjectPath}, assuming classic format", projectFilePath);
202 |             return false;
203 |         }
204 |     }
205 | 
206 |     private Microsoft.CodeAnalysis.Project? FindMostAppropriateProject(string filePath) {
207 |         if (!_solutionManager.IsSolutionLoaded) {
208 |             return null;
209 |         }
210 | 
211 |         var projects = _solutionManager.GetProjects().ToList();
212 |         if (!projects.Any()) {
213 |             return null;
214 |         }
215 | 
216 |         // Find projects where the file path is under the project directory
217 |         var projectsWithPath = new List<(Microsoft.CodeAnalysis.Project Project, int DirectoryLevel)>();
218 | 
219 |         foreach (var project in projects) {
220 |             if (string.IsNullOrEmpty(project.FilePath)) {
221 |                 continue;
222 |             }
223 | 
224 |             var projectDir = Path.GetDirectoryName(project.FilePath);
225 |             if (string.IsNullOrEmpty(projectDir)) {
226 |                 continue;
227 |             }
228 | 
229 |             if (filePath.StartsWith(projectDir, StringComparison.OrdinalIgnoreCase)) {
230 |                 // Calculate how many directories deep this file is from the project root
231 |                 var relativePath = filePath.Substring(projectDir.Length).TrimStart(Path.DirectorySeparatorChar);
232 |                 var directoryLevel = relativePath.Count(c => c == Path.DirectorySeparatorChar);
233 | 
234 |                 projectsWithPath.Add((project, directoryLevel));
235 |             }
236 |         }
237 | 
238 |         // Return the project where the file is closest to the root
239 |         // (smallest directory level means closer to project root)
240 |         return projectsWithPath.OrderBy(p => p.DirectoryLevel).FirstOrDefault().Project;
241 |     }
242 | 
243 |     public bool FileExists(string filePath) {
244 |         return File.Exists(filePath);
245 |     }
246 | 
247 |     public bool IsPathReadable(string filePath) {
248 |         var pathInfo = GetPathInfo(filePath);
249 |         return pathInfo.IsReadable;
250 |     }
251 | 
252 |     public bool IsPathWritable(string filePath) {
253 |         var pathInfo = GetPathInfo(filePath);
254 |         return pathInfo.IsWritable;
255 |     }
256 |     public bool IsCodeFile(string filePath) {
257 |         if (string.IsNullOrEmpty(filePath)) {
258 |             return false;
259 |         }
260 | 
261 |         // First check if file exists but is not part of the solution
262 |         if (File.Exists(filePath) && !IsReferencedBySolution(filePath)) {
263 |             return false;
264 |         }
265 | 
266 |         // Check by extension
267 |         var extension = Path.GetExtension(filePath);
268 |         return !string.IsNullOrEmpty(extension) && CodeFileExtensions.Contains(extension);
269 |     }
270 |     public PathInfo GetPathInfo(string filePath) {
271 |         if (string.IsNullOrEmpty(filePath)) {
272 |             return new PathInfo {
273 |                 FilePath = filePath,
274 |                 Exists = false,
275 |                 IsWithinSolutionDirectory = false,
276 |                 IsReferencedBySolution = false,
277 |                 IsFormattable = false,
278 |                 WriteRestrictionReason = "Path is empty or null"
279 |             };
280 |         }
281 | 
282 |         bool exists = File.Exists(filePath);
283 |         bool isWithinSolution = IsPathWithinSolutionDirectory(filePath);
284 |         bool isReferenced = IsReferencedBySolution(filePath);
285 |         bool isFormattable = IsCodeFile(filePath);
286 |         string? projectId = FindMostAppropriateProject(filePath)?.Id.Id.ToString();
287 | 
288 |         string? writeRestrictionReason = null;
289 | 
290 |         // Check for unsafe directories
291 |         if (ContainsUnsafeDirectory(filePath)) {
292 |             writeRestrictionReason = "Path contains a protected directory (bin, obj, .git, etc.)";
293 |         }
294 | 
295 |         // Check if file is outside solution
296 |         if (!isWithinSolution) {
297 |             writeRestrictionReason = "Path is outside the solution directory";
298 |         }
299 | 
300 |         // Check if directory is read-only
301 |         try {
302 |             var directoryPath = Path.GetDirectoryName(filePath);
303 |             if (!string.IsNullOrEmpty(directoryPath) && Directory.Exists(directoryPath)) {
304 |                 var dirInfo = new DirectoryInfo(directoryPath);
305 |                 if ((dirInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) {
306 |                     writeRestrictionReason = "Directory is read-only";
307 |                 }
308 |             }
309 |         } catch {
310 |             writeRestrictionReason = "Cannot determine directory permissions";
311 |         }
312 | 
313 |         return new PathInfo {
314 |             FilePath = filePath,
315 |             Exists = exists,
316 |             IsWithinSolutionDirectory = isWithinSolution,
317 |             IsReferencedBySolution = isReferenced,
318 |             IsFormattable = isFormattable,
319 |             ProjectId = projectId,
320 |             WriteRestrictionReason = writeRestrictionReason
321 |         };
322 |     }
323 | 
324 |     private bool IsPathWithinSolutionDirectory(string filePath) {
325 |         if (!_solutionManager.IsSolutionLoaded) {
326 |             return false;
327 |         }
328 | 
329 |         string? solutionDirectory = Path.GetDirectoryName(_solutionManager.CurrentSolution?.FilePath);
330 | 
331 |         if (string.IsNullOrEmpty(solutionDirectory)) {
332 |             return false;
333 |         }
334 | 
335 |         return filePath.StartsWith(solutionDirectory, StringComparison.OrdinalIgnoreCase);
336 |     }
337 | 
338 |     private bool IsReferencedBySolution(string filePath) {
339 |         if (!_solutionManager.IsSolutionLoaded || !File.Exists(filePath)) {
340 |             return false;
341 |         }
342 | 
343 |         // Check if the file is directly referenced by a document in the solution
344 |         if (_solutionManager.CurrentSolution!.GetDocumentIdsWithFilePath(filePath).Any()) {
345 |             return true;
346 |         }
347 | 
348 |         // TODO: Implement proper reference checking for assemblies, resources, etc.
349 |         // This would require deeper MSBuild integration
350 | 
351 |         return false;
352 |     }
353 | 
354 |     private bool ContainsUnsafeDirectory(string filePath) {
355 |         // Check if the path contains any unsafe directory segments
356 |         var normalizedPath = filePath.Replace('\\', '/');
357 |         var pathSegments = normalizedPath.Split('/');
358 | 
359 |         return pathSegments.Any(segment => UnsafeDirectories.Contains(segment));
360 |     }
361 |     private async Task<bool> TryFormatAndCommitFileAsync(Document document, CancellationToken cancellationToken, string commitMessage) {
362 |         try {
363 |             var formattedDocument = await _modificationService.FormatDocumentAsync(document, cancellationToken);
364 |             // Apply the formatting changes with the commit message
365 |             var newSolution = formattedDocument.Project.Solution;
366 |             await _modificationService.ApplyChangesAsync(newSolution, cancellationToken, commitMessage);
367 | 
368 |             _logger.LogInformation("Document {FilePath} formatted successfully", document.FilePath);
369 |             return true;
370 |         } catch (Exception ex) {
371 |             _logger.LogWarning(ex, "Failed to format file {FilePath}", document.FilePath);
372 |             return false;
373 |         }
374 |     }
375 | 
376 |     private static string TrimLeadingSpaces(string line) {
377 |         int i = 0;
378 |         while (i < line.Length && char.IsWhiteSpace(line[i])) {
379 |             i++;
380 |         }
381 | 
382 |         return i > 0 ? line.Substring(i) : line;
383 |     }
384 |     public async Task ProcessGitOperationsAsync(IEnumerable<string> filePaths, CancellationToken cancellationToken, string commitMessage) {
385 |         var filesList = filePaths.Where(f => !string.IsNullOrEmpty(f) && File.Exists(f)).ToList();
386 |         if (!filesList.Any()) {
387 |             return;
388 |         }
389 | 
390 |         try {
391 |             // Get solution path
392 |             var solutionPath = _solutionManager.CurrentSolution?.FilePath;
393 |             if (string.IsNullOrEmpty(solutionPath)) {
394 |                 _logger.LogDebug("Solution path is not available, skipping Git operations");
395 |                 return;
396 |             }
397 | 
398 |             // Check if solution is in a git repo
399 |             if (!await _gitService.IsRepositoryAsync(solutionPath, cancellationToken)) {
400 |                 _logger.LogDebug("Solution is not in a Git repository, skipping Git operations");
401 |                 return;
402 |             }
403 | 
404 |             _logger.LogDebug("Solution is in a Git repository, processing Git operations for {Count} files", filesList.Count);
405 | 
406 |             // Check if already on sharptools branch
407 |             if (!await _gitService.IsOnSharpToolsBranchAsync(solutionPath, cancellationToken)) {
408 |                 _logger.LogInformation("Not on a SharpTools branch, creating one");
409 |                 await _gitService.EnsureSharpToolsBranchAsync(solutionPath, cancellationToken);
410 |             }
411 | 
412 |             // Commit changes with the provided commit message
413 |             await _gitService.CommitChangesAsync(solutionPath, filesList, commitMessage, cancellationToken);
414 |             _logger.LogInformation("Git operations completed successfully for {Count} files with commit message: {CommitMessage}", filesList.Count, commitMessage);
415 |         } catch (Exception ex) {
416 |             // Log but don't fail the operation if Git operations fail
417 |             _logger.LogWarning(ex, "Git operations failed for {Count} files but file operations were still applied", filesList.Count);
418 |         }
419 |     }
420 | }
```
Page 1/4FirstPrevNextLast