diff --git a/.github/workflows/release-mcp-server.yml b/.github/workflows/release-mcp-server.yml
index aecd549c..0d14eaa0 100644
--- a/.github/workflows/release-mcp-server.yml
+++ b/.github/workflows/release-mcp-server.yml
@@ -96,26 +96,14 @@ jobs:
dotnet restore src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
dotnet restore src/ExcelMcp.CLI/ExcelMcp.CLI.csproj
- - name: Inject Application Insights Connection String
- run: |
- $telemetryFile = "src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs"
- $connectionString = "${{ secrets.APPINSIGHTS_CONNECTION_STRING }}"
-
- if ([string]::IsNullOrWhiteSpace($connectionString)) {
- Write-Output "⚠️ APPINSIGHTS_CONNECTION_STRING secret not configured - telemetry will be disabled"
- } else {
- Write-Output "Injecting Application Insights connection string..."
- $content = Get-Content $telemetryFile -Raw
- $content = $content -replace '__APPINSIGHTS_CONNECTION_STRING__', $connectionString
- Set-Content $telemetryFile $content
- Write-Output "✅ Telemetry connection string injected"
- }
- shell: pwsh
-
- name: Build MCP Server & CLI
run: |
dotnet build src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj --configuration Release --no-restore
dotnet build src/ExcelMcp.CLI/ExcelMcp.CLI.csproj --configuration Release --no-restore
+ env:
+ # Application Insights connection string is embedded at build time by MSBuild
+ # See ExcelMcp.McpServer.csproj for the GenerateTelemetryConfig target
+ APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
- name: Skip Tests (require Excel)
run: |
diff --git a/.github/workflows/release-vscode-extension.yml b/.github/workflows/release-vscode-extension.yml
index 30e3bcba..93d07dc6 100644
--- a/.github/workflows/release-vscode-extension.yml
+++ b/.github/workflows/release-vscode-extension.yml
@@ -75,28 +75,19 @@ jobs:
cd vscode-extension
npm install
- - name: Inject Application Insights Connection String
- run: |
- $telemetryFile = "src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs"
- $connectionString = "${{ secrets.APPINSIGHTS_CONNECTION_STRING }}"
-
- if ([string]::IsNullOrWhiteSpace($connectionString)) {
- Write-Output "⚠️ APPINSIGHTS_CONNECTION_STRING secret not configured - telemetry will be disabled"
- } else {
- Write-Output "Injecting Application Insights connection string..."
- $content = Get-Content $telemetryFile -Raw
- $content = $content -replace '__APPINSIGHTS_CONNECTION_STRING__', $connectionString
- Set-Content $telemetryFile $content
- Write-Output "✅ Telemetry connection string injected"
- }
- shell: pwsh
-
- name: Build and Package Extension
run: |
cd vscode-extension
# Run the package script which does: build:mcp-server + vsce package
npm run package
+ env:
+ # Application Insights connection string is embedded at build time by MSBuild
+ # See ExcelMcp.McpServer.csproj for the GenerateTelemetryConfig target
+ APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
+ - name: Prepare VSIX
+ run: |
+ cd vscode-extension
$version = "${{ env.PACKAGE_VERSION }}"
# vsce creates filename based on package.json name (excel-mcp)
diff --git a/.gitignore b/.gitignore
index 6c05ec8f..c4e29247 100644
--- a/.gitignore
+++ b/.gitignore
@@ -226,3 +226,11 @@ gh-pages/_site/*
codeql-db/*
infrastructure/azure/appinsights.secrets.local
+
+# Local environment files (secrets, connection strings)
+.env
+.env.local
+.env.*.local
+
+# Local MSBuild properties (secrets, connection strings)
+Directory.Build.props.user
diff --git a/Directory.Build.props b/Directory.Build.props
index e440a82d..649a8fc4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -49,4 +49,7 @@
+
+
+
diff --git a/Directory.Build.props.user.template b/Directory.Build.props.user.template
new file mode 100644
index 00000000..c147d1de
--- /dev/null
+++ b/Directory.Build.props.user.template
@@ -0,0 +1,17 @@
+
+
+
+
+
+ YOUR_CONNECTION_STRING_HERE
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 30aa27a7..626c530b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,7 +13,10 @@
-
+
+
+
+
@@ -37,4 +40,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 9d181c99..6401d0bd 100644
--- a/README.md
+++ b/README.md
@@ -140,3 +140,4 @@ This means you get:
### SEO & Discovery
`Excel Automation` • `Automate Excel with AI` • `MCP Server` • `Model Context Protocol` • `GitHub Copilot Excel` • `AI Excel Assistant` • `Power Query Automation` • `Power Query M Code` • `Power Pivot Automation` • `DAX Measures` • `DAX Automation` • `Data Model Automation` • `PivotTable Automation` • `VBA Automation` • `Excel Tables Automation` • `Excel AI Integration` • `COM Interop` • `Windows Excel Automation` • `Excel Development Tools` • `Excel Productivity` • `Excel Scripting` • `Conversational Excel` • `Natural Language Excel`
+# test
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 8caa098a..c61fa241 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -373,13 +373,23 @@ dotnet build -c Release
## 📊 **Application Insights / Telemetry Setup**
-ExcelMcp uses Azure Application Insights for anonymous usage telemetry and crash reporting. Telemetry is **opt-out** (enabled by default in release builds).
+ExcelMcp uses Azure Application Insights (Classic SDK with WorkerService integration) for anonymous usage telemetry and crash reporting. Telemetry is **opt-out** (enabled by default in release builds).
+
+### **How It Works**
+
+The Application Insights connection string is **embedded at build time** via MSBuild - there is no runtime environment variable lookup.
+
+**Build-time flow:**
+1. MSBuild reads `AppInsightsConnectionString` property (from `Directory.Build.props.user` or env var)
+2. Generates `TelemetryConfig.g.cs` with the connection string as a `const string`
+3. Compiled assembly contains the embedded connection string
### **What is Tracked**
- **Tool invocations**: Tool name, action, duration (ms), success/failure
- **Unhandled exceptions**: Exception type and redacted stack trace
-- **Session ID**: Random GUID per process (no user identification)
+- **User ID**: SHA256 hash of machine identity (anonymous, 16 chars)
+- **Session ID**: Random GUID per process (8 chars)
### **What is NOT Tracked**
@@ -396,6 +406,30 @@ All telemetry passes through `SensitiveDataRedactingProcessor` which removes:
- Connection string secrets (`Password=...` → `[REDACTED_CREDENTIAL]`)
- Email addresses → `[REDACTED_EMAIL]`
+### **Local Development with Telemetry**
+
+To enable telemetry in local builds:
+
+```powershell
+# 1. Copy the template file
+Copy-Item "Directory.Build.props.user.template" "Directory.Build.props.user"
+
+# 2. Edit Directory.Build.props.user and add your connection string
+# InstrumentationKey=xxx;IngestionEndpoint=...
+
+# 3. Build - connection string is embedded at compile time
+dotnet build src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
+
+# 4. Run - telemetry is automatically sent to Azure
+dotnet run --project src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
+```
+
+**Note:** `Directory.Build.props.user` is gitignored - your connection string won't be committed.
+
+### **Local Development without Telemetry**
+
+If you don't create `Directory.Build.props.user`, builds will have an empty connection string and telemetry will be disabled. This is the default for local development.
+
### **Azure Resources Setup (Maintainers Only)**
To deploy the Application Insights infrastructure:
@@ -419,106 +453,37 @@ After deploying Azure resources:
2. Add new secret: `APPINSIGHTS_CONNECTION_STRING`
3. Paste the connection string from deployment output
-The release workflow automatically injects this at build time.
-
-### **Local Development**
-
-During local development, telemetry is **disabled by default** because the placeholder connection string is not replaced. This is intentional - no telemetry data is sent from dev builds.
-
-#### **Debug Mode: Console Output**
-
-To test telemetry locally without Azure, enable debug mode which logs to stderr:
-
-```powershell
-# Enable debug telemetry (logs to console instead of Azure)
-$env:EXCELMCP_DEBUG_TELEMETRY = "true"
-
-# Build and run the MCP server
-dotnet build src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
-dotnet run --project src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
-
-# You'll see telemetry output like:
-# [Telemetry] Debug mode enabled - logging to stderr
-# Activity.TraceId: abc123...
-# Activity.DisplayName: ToolInvocation
-# Activity.Tags:
-# tool.name: excel_file
-# tool.action: list
-# tool.duration_ms: 42
-# tool.success: true
-```
-
-#### **Testing with Real Azure Resources**
-
-To test with actual Application Insights:
-
-```powershell
-# 1. Deploy Azure resources
-.\infrastructure\azure\deploy-appinsights.ps1 -SubscriptionId ""
-
-# 2. Temporarily inject connection string (DON'T COMMIT!)
-$connStr = "InstrumentationKey=xxx;IngestionEndpoint=https://..."
-(Get-Content "src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs") -replace `
- '__APPINSIGHTS_CONNECTION_STRING__', $connStr | `
- Set-Content "src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs"
-
-# 3. Build and run
-dotnet build src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
-dotnet run --project src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
-
-# 4. Check Azure Portal → Application Insights → Transaction search
-
-# 5. IMPORTANT: Revert the file (don't commit connection string!)
-git checkout src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs
-```
-
-To verify telemetry state:
-```csharp
-// ExcelMcpTelemetry.IsEnabled returns false when:
-// - Connection string is placeholder "__APPINSIGHTS_CONNECTION_STRING__"
-// - User has opted out via EXCELMCP_TELEMETRY_OPTOUT=true
-
-// ExcelMcpTelemetry.IsEnabled returns true when:
-// - EXCELMCP_DEBUG_TELEMETRY=true (console output mode)
-// - Connection string is real (injected at build time)
-```
-
-### **User Opt-Out**
-
-Users can disable telemetry by setting an environment variable:
-
-```powershell
-# Windows
-$env:EXCELMCP_TELEMETRY_OPTOUT = "true"
-
-# Or permanently via System Properties → Environment Variables
-```
+The release workflow sets this as an environment variable, and MSBuild embeds it at build time.
### **Telemetry Architecture**
-```
-MCP Tool Invocation
- │
- ▼
-ExcelToolsBase.ExecuteToolAction()
- │ (tracks: tool, action, duration, success)
- ▼
-ExcelMcpTelemetry.TrackToolInvocation()
- │
- ▼
-SensitiveDataRedactingProcessor
- │ (removes: paths, credentials, emails)
- ▼
-Azure Monitor Exporter → Application Insights
+```text
+Build Time:
+ MSBuild → reads AppInsightsConnectionString → generates TelemetryConfig.g.cs
+
+Runtime:
+ MCP Tool Invocation
+ │
+ ▼
+ ExcelMcpTelemetry.TrackToolInvocation()
+ │ (tracks: tool, action, duration, success)
+ ▼
+ SensitiveDataRedactingProcessor
+ │ (removes: paths, credentials, emails)
+ ▼
+ TelemetryClient → Application Insights
```
### **Files Overview**
| File | Purpose |
|------|---------|
-| `Telemetry/ExcelMcpTelemetry.cs` | Static helper for tracking |
+| `Telemetry/ExcelMcpTelemetry.cs` | Static helper for tracking events |
+| `Telemetry/ExcelMcpTelemetryInitializer.cs` | Sets User.Id and Session.Id on telemetry |
| `Telemetry/SensitiveDataRedactingProcessor.cs` | Redacts PII before transmission |
-| `Program.cs` | OpenTelemetry configuration |
+| `Program.cs` | Application Insights WorkerService configuration |
+| `ExcelMcp.McpServer.csproj` | MSBuild target that generates TelemetryConfig.g.cs |
+| `Directory.Build.props.user.template` | Template for local dev connection string |
| `infrastructure/azure/appinsights.bicep` | Azure resource definitions |
| `infrastructure/azure/deploy-appinsights.ps1` | Deployment script |
diff --git a/docs/EXCEPTION-PATTERN-MIGRATION.md b/docs/EXCEPTION-PATTERN-MIGRATION.md
deleted file mode 100644
index ce9205f9..00000000
--- a/docs/EXCEPTION-PATTERN-MIGRATION.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# Exception Pattern Migration - Test Fixes Required
-
-> **Status**: 82 test compilation errors (fixed 2 of 84, 97.6% remaining)
-> **Root Cause**: Commands converted to throw exceptions (void methods) but tests still use `var result = ` pattern
-> **Solution**: Systematically remove variable assignments for void method calls
-
-## Progress Summary
-
-**Completed**:
-- ✅ void Execute infrastructure added to IExcelBatch and ExcelBatch (non-breaking)
-- ✅ Documented all 84 errors with clear categorization
-- ✅ Fixed 2/84 errors (PowerQueryCommandsTests.cs: 4 fixes → 2 remaining)
-- ✅ Fixed helper fixtures (PowerQueryTestsFixture, TableTestsFixture, DataModelTestsFixture, PivotTableRealisticFixture)
-- ✅ Fixed FileCommandsTests.CreateEmptyThenList
-- ✅ Fixed SheetCommandsTests.Lifecycle (all 4 errors)
-- ✅ Fixed TableCommandsTests.cs (14 fixes - all void Create/Delete/Rename/etc patterns)
-
-**Remaining**: 82 errors across:
-- SheetCommandsTests.Move.cs (10 errors - Move, CopyToWorkbook, MoveToWorkbook calls)
-- SheetCommandsTests.TabColor.cs (2 errors)
-- SheetCommandsTests.Visibility.cs (6 errors)
-- PowerQueryCommandsTests.cs (16 remaining errors)
-- DataModelCommandsTests.cs (8 remaining errors - await void patterns)
-- PivotTableCommandsTests.Creation.cs (1 error)
-- PivotTableCommandsTests.OlapFields.cs (2 errors)
-
-## Error Summary
-
-Total errors: **82** (down from 84)
-
-### Error Types
-
-| Error Type | Count | Pattern | Fix |
-|-----------|-------|---------|-----|
-| CS0815 | 75 | `var result = _command.VoidMethod()` | Remove `var result = ` |
-| CS4008 | 9 | `await _command.VoidMethod()` | Remove `await` |
-
-### Affected Test Files
-
-| File | Line Count | Errors | Status |
-|------|-----------|--------|--------|
-| TableCommandsTests.cs | 500+ | 16 | 🔴 Pending |
-| SheetCommandsTests.Lifecycle.cs | 150+ | 4 | 🔴 Pending |
-| SheetCommandsTests.Move.cs | 370+ | 10 | 🔴 Pending |
-| SheetCommandsTests.TabColor.cs | 140+ | 2 | 🔴 Pending |
-| SheetCommandsTests.Visibility.cs | 180+ | 6 | 🔴 Pending |
-| PowerQueryCommandsTests.cs | 850+ | 20 | 🔴 Pending |
-| DataModelCommandsTests.cs | 320+ | 8 | 🔴 Pending |
-| PivotTableCommandsTests.Creation.cs | 80+ | 1 | 🔴 Pending |
-| PivotTableCommandsTests.OlapFields.cs | 250+ | 2 | 🔴 Pending |
-| PowerQueryTestsFixture.cs (helper) | 100+ | 3 | 🔴 Pending |
-| DataModelTestsFixture.cs (helper) | 150+ | 8 | 🔴 Pending |
-| PivotTableRealisticFixture.cs (helper) | 150+ | 2 | 🔴 Pending |
-| TableTestsFixture.cs (helper) | 120+ | 1 | 🔴 Pending |
-| FileCommandsTests.CreateEmptyThenList.cs | 100+ | 1 | 🔴 Pending |
-
-## Fix Pattern
-
-### Pattern 1: Simple Void Call (CS0815)
-
-**Before:**
-```csharp
-var result = await _commands.CreateAsync(batch, "Name");
-Assert.NotNull(result); // ❌ result is void
-```
-
-**After:**
-```csharp
-await _commands.CreateAsync(batch, "Name");
-// Exceptions throw if operation fails
-```
-
-### Pattern 2: Void Call Without Await (CS0815)
-
-**Before:**
-```csharp
-var result = _testHelper.SetupTable(batch, data);
-Assert.NotNull(result);
-```
-
-**After:**
-```csharp
-_testHelper.SetupTable(batch, data);
-// Exceptions throw if operation fails
-```
-
-### Pattern 3: Void with Await (CS4008)
-
-**Before:**
-```csharp
-var result = await _commands.DeleteAsync(batch, name);
-Assert.True(result.Success); // ❌ Can't await void
-```
-
-**After:**
-```csharp
-await _commands.DeleteAsync(batch, name);
-// Exceptions throw if operation fails
-```
-
-## Verification Strategy
-
-**Per-file approach:**
-1. Fix `var result = ` assignments to void methods
-2. Remove `Assert.NotNull(result)`, `Assert.True(result.Success)` checks (void methods throw on error)
-3. Keep assertions that verify actual Excel state (round-trip validation)
-4. Run `dotnet build` to confirm file's errors resolved
-5. Move to next file
-
-**Build validation:**
-```bash
-dotnet build # Must complete with 0 errors
-```
-
-## Commands for Bulk Search
-
-Find all `var result = ` assignments:
-```bash
-git grep -n "var result = " tests/ExcelMcp.Core.Tests/
-```
-
-Find all void method calls with assignment:
-```bash
-git grep -n "var .* = .*\..*Async(batch"
-```
-
-## Implementation Priority
-
-**Phase 1 (High Error Count):**
-1. PowerQueryCommandsTests.cs - 20 errors
-2. TableCommandsTests.cs - 16 errors
-3. SheetCommandsTests.Move.cs - 10 errors
-4. DataModelCommandsTests.cs - 8 errors + DataModelTestsFixture (8 errors)
-
-**Phase 2 (Helpers):**
-5. PowerQueryTestsFixture.cs - 3 errors
-6. PivotTableRealisticFixture.cs - 2 errors
-7. TableTestsFixture.cs - 1 error
-
-**Phase 3 (Remaining):**
-8. SheetCommandsTests.Lifecycle/TabColor/Visibility - 12 errors combined
-9. PivotTableCommandsTests - 3 errors
-10. FileCommandsTests.CreateEmptyThenList.cs - 1 error
-
-## Testing After Fixes
-
-Once all 84 errors are fixed:
-
-```bash
-# Build to verify compilation
-dotnet build
-
-# Run smoke test
-dotnet test --filter "FullyQualifiedName~McpServerSmokeTests.SmokeTest_AllTools_LlmWorkflow" --verbosity quiet
-
-# Run feature-specific tests
-dotnet test --filter "Feature=PowerQuery&RunType!=OnDemand"
-dotnet test --filter "Feature=Tables&RunType!=OnDemand"
-```
-
-## Notes
-
-- **No API changes needed** - void Execute infrastructure is complete
-- **Just test updates** - All test files need removal of `var result = ` patterns
-- **Error-first semantics** - Commands throw on error, tests no longer check `Success` flag
-- **Exception handling** - Try-catch in tests if specific error handling is needed (rare)
-
-## Rollback Procedure
-
-If needed:
-```bash
-git reset --hard HEAD~1 # Undo void Execute commit
-git checkout HEAD -- src/ExcelMcp.ComInterop/Session/IExcelBatch.cs
-git checkout HEAD -- src/ExcelMcp.ComInterop/Session/ExcelBatch.cs
-```
diff --git a/infrastructure/azure/appinsights-test.bicep b/infrastructure/azure/appinsights-test.bicep
new file mode 100644
index 00000000..52b7566f
--- /dev/null
+++ b/infrastructure/azure/appinsights-test.bicep
@@ -0,0 +1,58 @@
+// Application Insights infrastructure for ExcelMcp INTEGRATION TESTS
+// This is a separate instance from production for testing telemetry functionality
+//
+// Deploy with: az deployment sub create --location --template-file appinsights-test.bicep --parameters appinsights-test.parameters.json
+
+targetScope = 'subscription'
+
+@description('Name of the resource group to create')
+param resourceGroupName string = 'excelmcp-test-observability'
+
+@description('Azure region for all resources')
+param location string = 'westeurope'
+
+@description('Name of the Log Analytics workspace')
+param logAnalyticsName string = 'excelmcp-test-logs'
+
+@description('Name of the Application Insights resource')
+param appInsightsName string = 'excelmcp-test-appinsights'
+
+@description('Data retention in days - shorter for test instance to reduce costs')
+@minValue(30)
+@maxValue(730)
+param retentionInDays int = 30
+
+@description('Tags to apply to all resources')
+param tags object = {
+ project: 'ExcelMcp'
+ purpose: 'TelemetryTesting'
+ environment: 'test'
+ managedBy: 'Bicep'
+}
+
+// Resource Group
+resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
+ name: resourceGroupName
+ location: location
+ tags: tags
+}
+
+// Deploy resources into the resource group
+module observability 'appinsights-resources.bicep' = {
+ name: 'test-observability-deployment'
+ scope: rg
+ params: {
+ location: location
+ logAnalyticsName: logAnalyticsName
+ appInsightsName: appInsightsName
+ retentionInDays: retentionInDays
+ tags: tags
+ }
+}
+
+// Outputs
+output resourceGroupName string = rg.name
+output logAnalyticsWorkspaceId string = observability.outputs.logAnalyticsWorkspaceId
+output appInsightsName string = observability.outputs.appInsightsName
+output appInsightsConnectionString string = observability.outputs.appInsightsConnectionString
+output appInsightsInstrumentationKey string = observability.outputs.appInsightsInstrumentationKey
diff --git a/infrastructure/azure/appinsights-test.parameters.json b/infrastructure/azure/appinsights-test.parameters.json
new file mode 100644
index 00000000..1b260f28
--- /dev/null
+++ b/infrastructure/azure/appinsights-test.parameters.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "resourceGroupName": {
+ "value": "excelmcp-test-observability"
+ },
+ "location": {
+ "value": "swedencentral"
+ },
+ "logAnalyticsName": {
+ "value": "excelmcp-test-logs"
+ },
+ "appInsightsName": {
+ "value": "excelmcp-test-appinsights"
+ },
+ "retentionInDays": {
+ "value": 30
+ },
+ "tags": {
+ "value": {
+ "project": "ExcelMcp",
+ "purpose": "TelemetryTesting",
+ "environment": "test",
+ "managedBy": "Bicep"
+ }
+ }
+ }
+}
diff --git a/src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj b/src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
index cc4c383d..6e9d1253 100644
--- a/src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
+++ b/src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
@@ -14,6 +14,9 @@
$(NoWarn);CS1591
+
+ Sbroenne.ExcelMcp.McpServer.Tests
+
1.0.0
1.0.0.0
@@ -40,8 +43,44 @@
true
+
+
+
+ $(APPINSIGHTS_CONNECTION_STRING)
+
+
+
+
+
+
+
+
+
+ System.IO.File.WriteAllText(FilePath, Content);
+
+
+
+
+
+
+ $(IntermediateOutputPath)TelemetryConfig.g.cs
+ // Auto-generated at build time - do not edit
+namespace Sbroenne.ExcelMcp.McpServer.Telemetry%3B
+
+internal static class TelemetryConfig
+{
+ public const string ConnectionString = "$(AppInsightsConnectionString)"%3B
+}
+
+
+
+
+
+
+
+
@@ -79,8 +118,9 @@
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/ExcelMcp.McpServer/Program.cs b/src/ExcelMcp.McpServer/Program.cs
index 11306b88..bcb5b324 100644
--- a/src/ExcelMcp.McpServer/Program.cs
+++ b/src/ExcelMcp.McpServer/Program.cs
@@ -1,5 +1,6 @@
+using System.IO.Pipelines;
using Microsoft.ApplicationInsights;
-using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.ApplicationInsights.WorkerService;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -8,13 +9,39 @@
namespace Sbroenne.ExcelMcp.McpServer;
///
-/// ExcelCLI Model Context Protocol (MCP) Server
-/// Provides resource-based tools for AI assistants to automate Excel operations:
-///
+/// ExcelMCP Model Context Protocol (MCP) Server.
+/// Provides resource-based tools for AI assistants to automate Excel operations.
///
public class Program
{
- public static async Task Main(string[] args)
+ // Test transport configuration - set by tests before calling Main()
+ // These are intentionally static for test injection. Thread-safety is not required
+ // because tests run sequentially and call ResetTestTransport() after each test.
+ private static Pipe? _testInputPipe;
+ private static Pipe? _testOutputPipe;
+
+ ///
+ /// Configures the server to use in-memory pipe transport for testing.
+ /// Call this before RunAsync() to enable test mode.
+ ///
+ /// Pipe for reading client requests (client writes, server reads)
+ /// Pipe for writing server responses (server writes, client reads)
+ public static void ConfigureTestTransport(Pipe inputPipe, Pipe outputPipe)
+ {
+ _testInputPipe = inputPipe;
+ _testOutputPipe = outputPipe;
+ }
+
+ ///
+ /// Resets test transport configuration (call after test completes).
+ ///
+ public static void ResetTestTransport()
+ {
+ _testInputPipe = null;
+ _testOutputPipe = null;
+ }
+
+ public static async Task Main(string[] args)
{
// Register global exception handlers for unhandled exceptions (telemetry)
RegisterGlobalExceptionHandlers();
@@ -27,77 +54,113 @@ public static async Task Main(string[] args)
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
- // Configure OpenTelemetry for Application Insights (if not opted out)
+ // Configure Application Insights
ConfigureTelemetry(builder);
- // MCP Server architecture:
- // - Batch session management: LLM controls workbook lifecycle via begin/commit tools
-
- // Add MCP server with Excel tools (auto-discovers tools and prompts via attributes)
- builder.Services
- .AddMcpServer()
- .WithStdioServerTransport()
- .WithToolsFromAssembly();
-
- // Note: Completion support requires manual JSON-RPC method handling
- // See ExcelCompletionHandler for completion logic implementation
- // To enable: handle "completion/complete" method in custom transport layer
+ // Configure MCP Server - use test transport if configured, otherwise stdio
+ var mcpBuilder = builder.Services
+ .AddMcpServer(options =>
+ {
+ options.ServerInfo = new()
+ {
+ Name = "excel-mcp",
+ Version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0"
+ };
+
+ // Server-wide instructions for LLMs - helps with tool selection and workflow understanding
+ options.ServerInstructions = """
+ ExcelMCP automates Microsoft Excel via COM interop.
+
+ CRITICAL: File must be CLOSED in Excel desktop app (COM requires exclusive access).
+
+ SESSION LIFECYCLE:
+ 1. excel_file(action:'open') → returns sessionId
+ 2. Use sessionId with ALL subsequent tools
+ 3. excel_file(action:'close', save:true/false) → ONLY when completely done
+ Keep session open across multiple operations - don't close prematurely!
+ """;
+ })
+ .WithToolsFromAssembly()
+ .WithPromptsFromAssembly(); // Auto-discover prompts marked with [McpServerPromptType]
+
+ if (_testInputPipe != null && _testOutputPipe != null)
+ {
+ // Test mode: use in-memory pipe transport
+ mcpBuilder.WithStreamServerTransport(
+ _testInputPipe.Reader.AsStream(),
+ _testOutputPipe.Writer.AsStream());
+ }
+ else
+ {
+ // Production mode: use stdio transport
+ mcpBuilder.WithStdioServerTransport();
+ }
var host = builder.Build();
+ // Initialize telemetry client for static access
+ InitializeTelemetryClient(host.Services);
+
await host.RunAsync();
+
+ return 0;
}
///
- /// Configures Application Insights SDK for telemetry.
- /// Enables Users/Sessions/Funnels/User Flows analytics in Azure Portal.
- /// Respects opt-out via EXCELMCP_TELEMETRY_OPTOUT environment variable.
+ /// Initializes the static TelemetryClient from DI container.
///
- private static void ConfigureTelemetry(HostApplicationBuilder builder)
+ private static void InitializeTelemetryClient(IServiceProvider services)
{
- // Check if telemetry is enabled
- if (ExcelMcpTelemetry.IsOptedOut())
+ // Resolve TelemetryClient from DI and store for static access
+ // Worker Service SDK manages the TelemetryClient lifecycle including flush on shutdown
+ var telemetryClient = services.GetService();
+ if (telemetryClient != null)
{
- return; // User opted out
+ ExcelMcpTelemetry.SetTelemetryClient(telemetryClient);
}
+ }
- // Debug mode: log telemetry to stderr instead of Azure (for local testing)
- var isDebugMode = ExcelMcpTelemetry.IsDebugMode();
-
+ ///
+ /// Configures Application Insights Worker Service SDK for telemetry.
+ /// Uses AddApplicationInsightsTelemetryWorkerService() for proper host integration.
+ /// Enables Users/Sessions/Funnels/User Flows analytics in Azure Portal.
+ ///
+ private static void ConfigureTelemetry(HostApplicationBuilder builder)
+ {
var connectionString = ExcelMcpTelemetry.GetConnectionString();
- if (string.IsNullOrEmpty(connectionString) && !isDebugMode)
+ if (string.IsNullOrEmpty(connectionString))
{
- return; // No connection string available and not in debug mode
+ return; // No connection string available (local dev build)
}
- // Configure Application Insights SDK
- var aiConfig = TelemetryConfiguration.CreateDefault();
- if (!string.IsNullOrEmpty(connectionString))
- {
- aiConfig.ConnectionString = connectionString;
- }
- else if (isDebugMode)
+ // Configure Application Insights Worker Service SDK
+ // This provides:
+ // - Proper DI integration with IHostApplicationLifetime
+ // - Automatic dependency tracking
+ // - Automatic performance counter collection (where available)
+ // - Proper telemetry channel with ServerTelemetryChannel (retries, local storage)
+ // - Automatic flush on host shutdown
+ var aiOptions = new ApplicationInsightsServiceOptions
{
- // Debug mode without connection string - telemetry will be tracked but not sent
- // This allows testing the tracking code without Azure resources
- Console.Error.WriteLine("[Telemetry] Debug mode enabled - telemetry tracked locally (no Azure connection)");
- }
-
- // Add initializer to set User.Id and Session.Id on all telemetry
- aiConfig.TelemetryInitializers.Add(new ExcelMcpTelemetryInitializer());
-
- // Register TelemetryClient as singleton for dependency injection
- var telemetryClient = new TelemetryClient(aiConfig);
- builder.Services.AddSingleton(telemetryClient);
- builder.Services.AddSingleton(aiConfig);
+ // Set connection string if available
+ ConnectionString = connectionString,
+
+ // Disable features not needed for MCP server (reduces overhead)
+ EnableHeartbeat = true, // Useful for monitoring server health
+ EnableAdaptiveSampling = true, // Helps manage telemetry volume
+ EnableQuickPulseMetricStream = false, // Live Metrics not needed for CLI tool
+ EnablePerformanceCounterCollectionModule = false, // Perf counters not useful for short-lived CLI
+ EnableEventCounterCollectionModule = false, // Event counters not needed
+
+ // Disable dependency tracking for HTTP calls
+ EnableDependencyTrackingTelemetryModule = false,
+ };
- // Store reference for static access in ExcelMcpTelemetry
- ExcelMcpTelemetry.SetTelemetryClient(telemetryClient);
+ builder.Services.AddApplicationInsightsTelemetryWorkerService(aiOptions);
- if (isDebugMode)
- {
- Console.Error.WriteLine($"[Telemetry] Application Insights configured - User.Id={ExcelMcpTelemetry.UserId}, Session.Id={ExcelMcpTelemetry.SessionId}");
- }
+ // Add custom telemetry initializer for User.Id and Session.Id
+ // This enables the Users and Sessions blades in Azure Portal
+ builder.Services.AddSingleton();
}
///
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_connection.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_connection.md
deleted file mode 100644
index f72b59d1..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_connection.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# excel_connection Tool
-
-**Related tools**:
-- excel_powerquery – For Power Query M sources, CSV/TEXT/WEB imports, or when an OLE DB provider is unavailable
-
-**Actions**: list, view, create, test, refresh, delete, load-to, get-properties, set-properties
-
-**When to use excel_connection**:
-- Create and manage Excel connections (OLEDB, ODBC)
-- Refresh data from connection sources
-- Delete connections you no longer need
-- Load connection data to worksheets
-- Configure connection properties (background refresh, auto-refresh, etc.)
-- Use excel_powerquery for Power Query/M-driven sources, CSV/TEXT/WEB imports, or when the target provider is missing
-
-**Server-specific behavior**:
-- OLEDB connections: Fully supported when the provider is installed (e.g., Microsoft.ACE.OLEDB.16.0, SQLOLEDB). Excel throws "Value does not fall within the expected range" if the provider is missing.
-- ODBC connections: Supported; DSN or DSN-less connection strings work.
-- TEXT/WEB connections: Creation routed to Power Query (use excel_powerquery create)
-- DataFeed / Model: These types show up when workbook already has Power Query or Power Pivot connections. Manage/refresh them here; creation happens via excel_powerquery / Power Pivot UI.
-- Connection types 3 and 4 (TEXT/WEB) may report inconsistently
-- Delete removes connection and associated QueryTables
-- Power Query connections automatically redirect to excel_powerquery tool
-- Refresh/load-to actions time out after 5 minutes; if Excel is blocked (credentials, privacy dialog), you'll receive `SuggestedNextActions` instead of a hung session.
-
-**Action disambiguation**:
-- list: Show all connections in workbook
-- view: Display connection details and properties
-- create: Create OLEDB/ODBC connection (provider must exist). Use excel_powerquery for TEXT/WEB/Power Query scenarios.
-- test: Verify connection without refreshing data
-- refresh: Update data from connection source
-- delete: Remove connection and its QueryTables (use excel_powerquery delete for Power Query connections)
-- load-to: Load connection data to specified worksheet
-- get-properties: Retrieve connection properties (background query, refresh settings, etc.)
-- set-properties: Update connection properties (background query, refresh-on-open, save password, refresh period)
-
-**Common mistakes**:
-- Missing provider for OLEDB connection string → Install provider (e.g., ACE) or switch to ODBC/Power Query.
-- Trying to create Power Query/TEXT/WEB connections → Use excel_powerquery instead.
-- Not testing connection before refresh → Use test first to verify connectivity
-
-**Workflow optimization**:
-- Provide concrete provider connection strings (e.g., `Provider=Microsoft.ACE.OLEDB.16.0;...`).
-- Use test before refresh to surface provider/network errors earlier.
-- Route CSV/TEXT/WEB scenarios to excel_powerquery (faster automation, no provider requirement).
-- Check properties with get-properties before modifying via set-properties.
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_datamodel.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_datamodel.md
deleted file mode 100644
index 2740b3b2..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_datamodel.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# excel_datamodel - Server Quirks
-
-**Action disambiguation**:
-- list-tables: Data model tables only (NOT worksheet tables)
-- read-table: Detailed info (columns, measures, row count, refresh date)
-- list-columns: Columns in specific data model table
-- create-measure vs DAX formulas: Measures are data model calculations, not worksheet formulas
-
-**Server-specific quirks**:
-- Requires data loaded with loadDestination='data-model' or 'both' first
-- DAX syntax (Power Pivot) not Excel worksheet syntax
-- Measures created in data model, NOT visible in worksheets until used in PivotTable
-- formatString: Must set explicitly or numbers display as general format
-- list-* actions have a 5 minute guard. If Power Pivot is waiting on a dialog, you'll get timeout guidance (SuggestedNextActions) instead of a silent hang—surface it before retrying.
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_file.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_file.md
deleted file mode 100644
index 4c9a83e8..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_file.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# excel_file Tool
-
-**Actions**: open, close, create-empty, close-workbook, test
-
-**IMPORTANT: NO 'save' ACTION**
-- To persist changes, use: `action='close'` with `save=true` parameter
-- Common mistake: `action='save'` (WRONG) → use `action='close', save=true` (CORRECT)
-
-**CRITICAL: DO NOT CLOSE SESSION PREMATURELY**
-- **ONLY close when ALL operations are complete** - closing mid-workflow loses the session
-- If user requests multiple operations, keep session open until explicitly told to close
-- Ask user "Should I close the session now?" if workflow completion is unclear
-- Premature close causes: lost session, file locks, failed subsequent operations
-
-**Session lifecycle** (REQUIRED for all operations):
-1. `action='open'` → returns sessionId
-2. Use sessionId with other excel_* tools
-3. `action='close', save=true` → persists changes and ends session
-4. `action='close', save=false` → discards changes and ends session
-
-**Related tools**:
-- excel_worksheet - Add sheets after creating workbook
-- excel_powerquery - Load data into workbook
-- excel_range - Write data to workbook
-- excel_vba - Use .xlsm extension for macro-enabled workbooks
-
-**When to use excel_file**:
-- Start/end sessions for Excel operations
-- Create new blank Excel workbooks
-- Validate file exists and is accessible
-
-**Server-specific behavior**:
-- open: Creates session, returns sessionId (required for all operations)
-- close: Ends session, optional save parameter (default: false)
-- create-empty: Creates .xlsx or .xlsm (specify extension for macro support)
-- close-workbook: No-op (deprecated - automatic with single-instance architecture)
-- test: Validates file without opening with Excel COM
-- **File locking**: All Excel operations automatically check if file is locked and return clear error if open
-
-**Action disambiguation**:
-- open: Start session for operations
-- close (save=true): Persist changes and end session
-- close (save=false): Discard changes and end session
-- create-empty: New blank workbook
-- test: Check if file exists and has valid extension
-- close-workbook: Deprecated (automatic cleanup)
-
-**Common mistakes**:
-- Using `action='save'` → WRONG! Use `action='close', save=true`
-- Forgetting .xlsm for VBA → Specify extension in path
-- Not starting session → All operations require sessionId from 'open' action
-
-**Workflow optimization**:
-- After create-empty: Use 'open' to start session
-- After operations: Use 'close' with save=true to persist
-
-
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_namedrange.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_namedrange.md
deleted file mode 100644
index aff85cf3..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_namedrange.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# excel_namedrange Tool
-
-**Related tools**:
-- excel_range - For reading/writing data in named ranges (use sheetName="")
-- excel_powerquery - Named ranges can be used as Power Query parameters
-
-**Actions**: list, get, set, create, update, delete, create-bulk
-
-**When to use excel_namedrange**:
-- Named ranges as configuration parameters
-- Reusable cell references
-- Settings, thresholds, dates
-- Use excel_range for data operations
-- Use excel_namedrange interchangeably (same tool)
-
-**Server-specific behavior**:
-- Parameters are named ranges pointing to single cells
-- create-bulk: Efficient multi-parameter creation (one call vs many)
-- Absolute references recommended: =Sheet1!$A$1
-- Parameters accessible across entire workbook
-
-**Action disambiguation**:
-- create: Add single named range parameter
-- create-bulk: Add multiple parameters in one call (90% faster)
-- get: Retrieve parameter value
-- set: Update parameter value
-- update: Change parameter cell reference
-
-**Common mistakes**:
-- Creating parameters one-by-one → Use create-bulk for 2+ parameters
-- Missing = prefix in references → Must be =Sheet1!$A$1 not Sheet1!$A$1
-- Relative references → Use absolute ($A$1) for parameters
-
-**Workflow optimization**:
-- Multiple parameters? Use create-bulk action
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_pivottable.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_pivottable.md
deleted file mode 100644
index 0b4d31f2..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_pivottable.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# excel_pivottable Tool
-
-**Related tools**:
-- excel_range - For source data ranges (use create-from-range)
-- excel_table - For structured source tables (use create-from-table)
-- excel_datamodel - For Data Model tables with DAX (use create-from-datamodel)
-
-**Actions**: list, get-info, create-from-range, create-from-table, create-from-datamodel, delete, refresh, list-fields, add-row-field, add-column-field, add-value-field, add-filter-field, remove-field, set-field-function, set-field-name, set-field-format, set-field-filter, sort-field, get-data
-
-**When to use excel_pivottable**:
-- Create PivotTables from ranges or tables
-- Configure PivotTable fields and calculations
-- Use excel_table for source data tables
-- Use excel_datamodel for DAX-based analytics
-
-**Server-specific behavior**:
-- Requires source data in range or table
-- PivotTables auto-refresh when source data changes
-- Fields can be rows, columns, values, or filters
-- Value field functions: Sum, Count, Average, Max, Min, etc.
-- create-from-datamodel enforces a 5 minute timeout; if the Data Model is stuck (privacy dialogs, refresh), you'll get `SuggestedNextActions` to resolve it instead of a hung session.
-
-**Action disambiguation**:
-- create-from-range: Create PivotTable from range address (sheetName + range parameters)
-- create-from-table: Create PivotTable from Excel Table/ListObject (excelTableName = worksheet table name)
-- create-from-datamodel: Create PivotTable from Power Pivot Data Model table (dataModelTableName = Data Model table name)
-- add-row-field: Field goes to row area
-- add-column-field: Field goes to column area
-- add-value-field: Field goes to values area (calculations)
- - **OLAP Mode 1**: Add pre-existing DAX measure (fieldName = measure name or "[Measures].[Name]")
- - **OLAP Mode 2**: Auto-create DAX measure from column (fieldName = column name, specify aggregation function)
- - **Regular PivotTables**: Add column to values area with aggregation function
-- add-filter-field: Field goes to filter area
-- set-field-function: Change aggregation (Sum, Count, etc.)
-
-**add-value-field for OLAP PivotTables**:
-- **Pre-existing measure**: Use fieldName = "Total Sales" or "[Measures].[Total Sales]"
- - Adds existing measure without creating duplicate
- - aggregationFunction parameter ignored (measure formula defines aggregation)
- - Best for complex DAX measures with relationships, time intelligence, etc.
-- **Auto-create from column**: Use fieldName = "Sales" (column name)
- - Creates new DAX measure: SUM('Table'[Sales]), COUNT('Table'[Column]), etc.
- - aggregationFunction parameter determines DAX function (Sum, Count, Average, etc.)
- - customName parameter sets measure name
- - Best for simple aggregations
-
-**Parameter guidance for create actions**:
-- create-from-range: sheetName, range, destinationSheet, destinationCell, pivotTableName
-- create-from-table: excelTableName (Excel Table name), destinationSheet, destinationCell, pivotTableName
-- create-from-datamodel: dataModelTableName (Data Model table name), destinationSheet, destinationCell, pivotTableName
-
-**Common mistakes**:
-- Creating PivotTable without source data → Prepare data first
-- Not refreshing after source data changes → Use refresh action
-- Wrong field type → Rows vs Columns vs Values have different purposes
-- **OLAP**: Using column name when measure exists → Use measure name directly for existing measures
-- **OLAP**: Creating duplicate measures → Check if measure exists first with excel_datamodel list-measures
-
-**Workflow optimization**:
-- Pattern: Create → Add fields → Set functions → Format → Sort
-- **OLAP with existing measures**: list-fields → add-value-field (use measure name) → format
-- **OLAP auto-create**: add-value-field (use column name + aggregation) → creates measure automatically
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_powerquery.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_powerquery.md
index 2cd9e6f6..4e142f48 100644
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_powerquery.md
+++ b/src/ExcelMcp.McpServer/Prompts/Content/excel_powerquery.md
@@ -1,21 +1,25 @@
# excel_powerquery - Server Quirks
**Action disambiguation**:
+
- create: Import NEW query using inline `mCode` (FAILS if query already exists - use update instead)
- update: Update EXISTING query M code + refresh data (use this if query exists)
- load-to: Loads to worksheet or data model or both (not just config change) - CHECKS for sheet conflicts
- unload: Removes data from worksheet but keeps query definition (inverse of load-to)
**When to use create vs update**:
+
- Query doesn't exist? → Use create
- Query already exists? → Use update (create will error "already exists")
- Not sure? → Check with list action first, then use update if exists or create if new
**Inline M code**:
+
- Provide raw M code directly via `mCode`
- Keep `.pq` files only for GIT workflows
**Create/LoadTo with existing sheets**:
+
- Use `targetCellAddress` to place the table on an existing worksheet without deleting other content
- Applies to BOTH create and load-to
- If the worksheet already has data and you omit `targetCellAddress`, the tool returns guidance telling you to provide one
@@ -23,11 +27,13 @@
- Worksheets that exist but are empty behave like new sheets (default destination = A1)
**Common mistakes**:
+
- Using create on existing query → ERROR "Query 'X' already exists" (should use update)
- Using update on new query → ERROR "Query 'X' not found" (should use create)
- Calling LoadTo without checking if sheet exists (will error if sheet exists)
**Server-specific quirks**:
+
- Validation = execution: M code only validated when data loads/refreshes
- connection-only queries: NOT validated until first execution
- refresh with loadDestination: Applies load config + refreshes (2-in-1)
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_range.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_range.md
deleted file mode 100644
index 83685b02..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_range.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# excel_range Tool
-
-**Related tools**:
-- excel_table - For structured tables with AutoFilter and structured references
-- excel_namedrange - For defining reusable range names
-- excel_worksheet - For sheet lifecycle (create, delete, rename)
-
-**Actions**: get-values, set-values, get-formulas, set-formulas, get-number-formats, set-number-format, set-number-formats, clear-all, clear-contents, clear-formats, copy, copy-values, copy-formulas, insert-cells, delete-cells, insert-rows, delete-rows, insert-columns, delete-columns, find, replace, sort, get-used-range, get-current-region, get-range-info, add-hyperlink, remove-hyperlink, list-hyperlinks, get-hyperlink, get-style, set-style, format-range, validate-range, get-validation, remove-validation, autofit-columns, autofit-rows, merge-cells, unmerge-cells, get-merge-info, add-conditional-formatting, clear-conditional-formatting, set-cell-lock, get-cell-lock
-
-**When to use excel_range**:
-- Cell values, formulas, formatting in existing worksheets
-- Use excel_table for structured tables (AutoFilter, structured refs)
-- Use excel_powerquery for external data sources
-- Use excel_worksheet for sheet lifecycle (create, delete, rename)
-
-**Server-specific behavior**:
-- Single cell returns [[value]] as 2D array, not scalar
-- Named ranges: use sheetName="" (empty string)
-- For performance: Use set-number-formats (plural) to format multiple cells at once
-
-**Action disambiguation**:
-- clear-all: Removes content + formatting
-- clear-contents: Removes content only, preserves formatting
-- clear-formats: Removes formatting only, preserves content
-- copy: Copies everything (values + formulas + formatting)
-- copy-values: Copies only values (no formulas, no formatting)
-- copy-formulas: Copies only formulas (no values, no formatting)
-- set-number-format: Apply one format code to entire range
-- set-number-formats: Apply different format codes to each cell (2D array)
-- get-style: Inspect built-in Excel style currently applied to a range (returns style name and type)
-- set-style: Apply built-in Excel style (Heading 1, Total, Input, etc.) - RECOMMENDED for formatting
-- format-range: Apply custom formatting (font, color, borders) - Use only when built-in styles don't fit
-- get-used-range: Returns actual data bounds (ignores empty cells)
-- get-current-region: Returns contiguous region around a cell
-
-**Common mistakes**:
-- Expecting single cell to return scalar → Always returns 2D array [[value]]
-- Using sheetName for named ranges → Use sheetName="" for named ranges
-- Setting number format per cell → Use set-number-format for entire range instead
-- **List validation with comma-separated values → Only range references create dropdowns! Write values to range first, then reference it.**
-
-**Workflow optimization**:
-- Combine operations: set values + format + validate in one session
-- Use get-used-range to discover data bounds before operations
-- **Inspecting styles?** Use get-style to check current formatting before making changes
-- **Formatting?** Try set-style with built-in styles first (faster, theme-aware, consistent)
-- Only use format-range for brand-specific colors or one-off custom designs
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_table.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_table.md
deleted file mode 100644
index b7e1692a..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_table.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# excel_table - Server Quirks
-
-**Action disambiguation**:
-- add-to-datamodel vs loadDestination: add-to-datamodel for existing Excel tables, loadDestination for Power Query imports
-- resize: Changes table boundaries (not the same as append)
-- get-structured-reference: Returns formula + range address for use with excel_range
-- set-style: Apply built-in Excel table styles (60+ presets available)
-- get-data: Returns row data (set `visibleOnly: true` to honor active filters)
-
-**Server-specific quirks**:
-- Column names: Any string including purely numeric (e.g., "60")
-- Table names: Must start with letter/underscore, alphanumeric only
-- AutoFilter: Enabled by default on table creation
-- Structured references: =Table1[@ColumnName] (auto-adjusts when table resizes)
-- Table styles: Can be applied during create or changed later with set-style
-- get-data with `visibleOnly=true` returns only rows still visible after filters (same order as Excel)
-
-**Table Style Categories** (60+ built-in styles):
-- **Light** (TableStyleLight1-21): Subtle colors, minimal formatting
-- **Medium** (TableStyleMedium1-28): Balanced colors, most popular
-- **Dark** (TableStyleDark1-11): High contrast, bold colors
-
-**Popular table styles**:
-- TableStyleMedium2 - orange accents (most common)
-- TableStyleMedium9 - gray/blue professional
-- TableStyleLight9 - blue banding (subtle)
-- TableStyleDark1 - dark blue header (high contrast)
-
-**Style behavior**:
-- Styles include header formatting, row banding, total row formatting
-- Changing style preserves data and structure
-- Empty string "" removes table styling
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_vba.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_vba.md
deleted file mode 100644
index 25223edf..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_vba.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# excel_vba Tool
-
-**Related tools**:
-- excel_file - Create .xlsm files (macro-enabled workbooks)
-- excel_range - For reading/writing data from VBA procedures
-
-**Actions**: list, view, import, export, update, delete, run
-
-**When to use excel_vba**:
-- VBA macro management
-- Import/export VBA modules for version control
-- Run existing macros
-- Use excel_range for data operations
-- Requires .xlsm files (macro-enabled)
-
-**Server-specific behavior**:
-- Requires macro-enabled workbook (.xlsm)
-- VBA trust settings must allow programmatic access
-- Modules stored as .bas or .cls files
-- run action executes VBA Sub procedures
-
-**Action disambiguation**:
-- list: Show all VBA modules in workbook
-- view: Get VBA code for specific module
-- import: Load VBA code from .bas/.cls file
-- export: Save VBA module to file (version control)
-- run: Execute VBA Sub procedure
-- update: Replace module code
-
-**Common mistakes**:
-- Using .xlsx instead of .xlsm → VBA requires macro-enabled files
-- VBA trust not enabled → Check security settings
-- Running Function instead of Sub → Use run for Sub only
-
-**Workflow optimization**:
-- Version control: export → Git → import on other machine
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/excel_worksheet.md b/src/ExcelMcp.McpServer/Prompts/Content/excel_worksheet.md
deleted file mode 100644
index 3769f86b..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/excel_worksheet.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# excel_worksheet Tool
-
-**Related tools**:
-- excel_range - For data operations on worksheet cells
-- excel_table - For structured tables on worksheets
-- excel_powerquery - For loading external data to worksheets
-
-**Actions**: list, create, rename, copy, delete, move, copy-to-workbook, move-to-workbook, set-tab-color, get-tab-color, clear-tab-color, hide, very-hide, show, get-visibility, set-visibility
-
-**When to use excel_worksheet**:
-- Sheet lifecycle (create, delete, rename, copy, move)
-- Copy/move sheets within same workbook
-- Copy/move sheets between different workbooks
-- Sheet visibility (hide, show)
-- Sheet tab colors
-- Use excel_range for data operations
-- Use excel_powerquery for external data loading
-
-## Cross-Workbook Operations
-
-**When to use copy-to-workbook vs copy**:
-- `copy`: Duplicate sheet WITHIN same workbook (single sessionId)
-- `copy-to-workbook`: Copy sheet TO DIFFERENT workbook (requires TWO sessionIds)
-- `move-to-workbook`: Move sheet TO DIFFERENT workbook (requires TWO sessionIds)
-
-**Cross-workbook workflow**:
-1. Open source file: `excel_file(action: 'open', excelPath: 'source.xlsx')` → get sessionId1
-2. Open target file: `excel_file(action: 'open', excelPath: 'target.xlsx')` → get sessionId2
-3. Copy/move sheet: `excel_worksheet(action: 'copy-to-workbook', sessionId: sessionId1, targetSessionId: sessionId2, sheetName: 'Sales', targetName: 'Q1_Sales')`
-
-**Parameters for cross-workbook**:
-- `sessionId`: Source workbook session
-- `targetSessionId`: Target workbook session
-- `sheetName`: Sheet to copy/move from source
-- `targetName` (optional): Rename during copy/move
-- `beforeSheet` OR `afterSheet` (optional): Position in target workbook
-
-**Example - Copy sheet to consolidation workbook**:
-```
-User: "Copy the Sales sheet from Q1.xlsx to Annual_Report.xlsx"
-
-1. Open Q1.xlsx → sessionId: "abc123"
-2. Open Annual_Report.xlsx → sessionId: "def456"
-3. excel_worksheet(
- action: 'copy-to-workbook',
- sessionId: 'abc123',
- targetSessionId: 'def456',
- sheetName: 'Sales',
- targetName: 'Q1_Sales'
- )
-```
-
-**Server-specific behavior**:
-- Cannot delete last remaining worksheet (Excel limitation)
-- Cannot delete active worksheet while viewing in Excel UI
-- very-hide: Hidden from UI and VBA (stronger than hide)
-
-**Action disambiguation**:
-- create: Add new blank worksheet
-- copy: Duplicate existing worksheet WITHIN same workbook
-- move: Reposition existing worksheet WITHIN same workbook
-- copy-to-workbook: Copy sheet TO DIFFERENT workbook (requires targetSessionId)
-- move-to-workbook: Move sheet TO DIFFERENT workbook (requires targetSessionId)
-- hide: Hide from UI tabs (visible in VBA)
-- very-hide: Hide from UI and VBA (stronger protection)
-- show: Make visible in UI tabs
-
-**Common mistakes**:
-- Trying to delete last worksheet → Excel requires at least one sheet
-- Not checking if sheet exists before operations → Use list first
-- Using copy when files are different → Use copy-to-workbook instead
-- Forgetting to open both files before cross-workbook operation → Both need sessionIds
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/server_quirks.md b/src/ExcelMcp.McpServer/Prompts/Content/server_quirks.md
deleted file mode 100644
index bc0dc51a..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/server_quirks.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Server Quirks & Gotchas - Things I Need to Remember
-
-## CRITICAL: File Access Requirements
-
-**NEVER work on files that are already open in Excel**
-- ExcelMcp requires EXCLUSIVE access to workbooks
-- If file is open in Excel UI or another process → operations WILL FAIL
-- Error: "The file is already open in Excel or another process is using it"
-- **USER ACTION REQUIRED**: Tell user to close the file first!
-- This is NOT optional - Excel COM automation requires exclusive access
-
-**Why this matters:**
-- Excel uses file locking to prevent corruption
-- Multiple processes accessing same file = data loss risk
-- Automation needs predictable state (no user edits during operation)
-
-**How to detect:**
-- User says "the file is open" or "I have Excel running"
-- Error message mentions "already open" or "locked by another process"
-- ALWAYS tell user: "Please close the file in Excel before running automation"
-
-## Data Type Surprises
-
-**Single cells return 2D arrays, not scalars**
-- excel_range(get-values, rangeAddress='A1') → [[42]] not 42
-- ALWAYS expect [[value]] even for single cell
-- I must extract value[0][0] if I need the scalar
-
-**Named ranges use empty sheetName**
-- excel_range(rangeAddress='SalesData', sheetName='') ← empty string!
-- NOT sheetName='Sheet1' for named ranges
-
-## Excel COM Limitations
-
-**Cannot create OLEDB/ODBC connections programmatically**
-- excel_connection can only MANAGE existing connections
-- User must create OLEDB/ODBC in Excel UI first
-- TEXT connections work fine for automation
-
-**Cannot delete last worksheet**
-- Excel always requires at least one sheet
-- excel_worksheet(delete) will fail if it's the last one
-
-**VBA requires .xlsm files**
-- excel_vba won't work on .xlsx
-- File must be macro-enabled
-
-## Load Destination Confusion
-
-**'worksheet' vs 'data-model' vs 'both'**
-- worksheet: Users see data, NO DAX capability
-- data-model: Ready for DAX, users DON'T see data
-- both: Users see data AND DAX works
-- DEFAULT is 'worksheet' if not specified
-
-**Cannot directly add worksheet query to Data Model**
-- If loaded to worksheet only, can't use excel_table add-to-datamodel
-- Must use excel_powerquery set-load-to-data-model to fix
-
-## Number Format Edge Cases
-
-**Format codes are strings, not patterns**
-- '$#,##0.00' is exact string, not regex
-- Common codes: see format_codes.md completion
-
-**set-number-format vs set-number-formats (plural)**
-- set-number-format: ONE format for entire range
-- set-number-formats: DIFFERENT format per cell (2D array)
-
-## Common Error Patterns
-
-**"Value does not fall within expected range"**
-- Usually: Trying to create OLEDB connection (can't do it)
-- Or: Invalid range address
-- Or: Operation not supported by Excel COM
-
-**"Refresh failed or data not updated"**
-- RefreshAll() is async and unreliable
-- Solution: Use individual query/connection refresh (synchronous)
-
-**"Parameter name missing ="**
-- Named ranges must be: =Sheet1!$A$1
-- NOT: Sheet1!$A$1 (missing = prefix)
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/tool_selection_guide.md b/src/ExcelMcp.McpServer/Prompts/Content/tool_selection_guide.md
deleted file mode 100644
index bc260d9f..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/tool_selection_guide.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Tool Selection Guide
-
-## Pre-Requisite
-- File must be CLOSED in Excel (exclusive access required by COM)
-- Use `excel_file(open)` to start a session, `excel_file(close)` to end
-
-**CRITICAL - Session Lifetime Rules:**
-- **KEEP session open** across multiple operations
-- **ONLY close** when user explicitly requests it OR all operations are complete
-- **ASK user** "Should I close the session now?" if unclear whether more operations are needed
-- Closing mid-workflow = lost session, cannot resume
-
-
-## Quick Reference
-
-| Need | Use | NOT |
-|------|-----|-----|
-| External data (databases, APIs, CSV) | `excel_powerquery` + `loadDestination` | `excel_table` (data already in Excel) |
-| Connection management | `excel_connection` | - |
-| DAX measures / Data Model | `excel_datamodel` | `excel_range` (worksheet formulas) |
-| Data in worksheets (values, formulas, format) | `excel_range` | - |
-| Convert range to table | `excel_table` | - |
-| Sheet lifecycle (create, delete, hide, rename) | `excel_worksheet` | - |
-| Named ranges (parameters) | `excel_namedrange` (use `create-bulk` for 2+) | - |
-| VBA macros (.xlsm only) | `excel_vba` | - |
-| PivotTables | `excel_pivottable` | - |
-
-## Common Mistakes
-
-**Don't use `excel_table` for external data**
-- WRONG: `excel_table(create)` for CSV import
-- CORRECT: `excel_powerquery(create, loadDestination='worksheet')`
-
-**loadDestination matters**
-- WRONG: `excel_powerquery` without `loadDestination` for DAX
-- CORRECT: `excel_powerquery(create, loadDestination='data-model')`
-
-**Use bulk operations for multiple items**
-- WRONG: `excel_namedrange(create)` called 5 times
-- CORRECT: `excel_namedrange(create-bulk)` with JSON array
-
-**DAX is not worksheet formulas**
-- WRONG: Using `excel_range` for DAX syntax
-- CORRECT: `excel_datamodel(create-measure)` with DAX
-
-**WorksheetAction vs DAX**
-- Worksheet formulas: `excel_range` with `=SUM(A1:A10)`
-- DAX measures: `excel_datamodel` with `SUM(Sales[Amount])`
diff --git a/src/ExcelMcp.McpServer/Prompts/Content/user_request_patterns.md b/src/ExcelMcp.McpServer/Prompts/Content/user_request_patterns.md
deleted file mode 100644
index 67441e0c..00000000
--- a/src/ExcelMcp.McpServer/Prompts/Content/user_request_patterns.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# Common User Request Patterns - How to Interpret
-
-## PRE-FLIGHT: File Access Check
-
-**BEFORE processing ANY request, check if file is accessible:**
-
-**User signals file might be open:**
-- "I have the file open" → STOP, tell them to close it
-- "The file is currently open in Excel" → STOP
-- "Can I run this while viewing the file?" → NO, tell them to close first
-- "Error says file is locked" → File is open, tell them to close it
-
-**Always ask if unsure:**
-- "Is the Excel file currently open?"
-- "Please close the file before we proceed with automation"
-
-**This is mandatory - Excel COM automation requires exclusive file access!**
-
-## Data Import Requests
-
-**"Load this CSV file"**
-→ excel_powerquery(create, sourcePath='file.csv', loadDestination='worksheet')
-→ NOT excel_table (that's for existing data)
-
-**"Import data from SQL Server"**
-→ excel_connection(create) with OLEDB connection string
-→ Then excel_powerquery for transformations
-
-**"Load Power Query results to worksheet"**
-→ excel_powerquery(create, loadDestination='worksheet')
-
-**"Put data in Data Model for DAX"**
-→ excel_powerquery(create, loadDestination='data-model')
-→ NOT 'worksheet' (that won't work for DAX)
-
-**"Refresh data from external source"**
-→ excel_powerquery(refresh) - synchronous, guaranteed persistence
-→ excel_connection(refresh) - for connection-based data
-
-## Formatting Requests
-
-**"Make headers bold with blue background"**
-→ excel_range(format-range, bold=true, fillColor='#4472C4', fontColor='#FFFFFF')
-→ Single call with multiple properties
-
-**"Format column D as currency"**
-→ excel_range(set-number-format, rangeAddress='D:D', formatCode='$#,##0.00')
-
-**"Add dropdown for Status column"**
-→ excel_range(validate-range, validationType='list', validationFormula1='Active,Inactive,Pending')
-
-## Analytics Requests
-
-**"Create Total Sales measure"**
-→ First: Check data in Data Model with excel_datamodel(list-tables)
-→ Then: excel_datamodel(create-measure, daxFormula='SUM(Sales[Amount])')
-
-**"Link Sales to Products table"**
-→ excel_datamodel(create-relationship, fromTable='Sales', fromColumn='ProductID', toTable='Products', toColumn='ProductID')
-
-## Configuration Requests
-
-**"Set up parameters for date range"**
-→ excel_namedrange(create-bulk) with StartDate and EndDate
-→ NOT excel_range (parameters are named ranges)
-
-## Structure Requests
-
-**"Create new sheet called Reports"**
-→ excel_worksheet(create, sheetName='Reports')
-
-**"Convert this data to a table"**
-→ excel_table(create, sourceRange='A1:E100', tableName='SalesData')
-
-## VBA Requests
-
-**"Export VBA code for version control"**
-→ excel_vba(export, moduleName='Module1', targetPath='Module1.bas')
-
-**"Import macro from file"**
-→ excel_vba(import, sourcePath='Module1.bas')
-→ File must be .xlsm
-
-## Discovery Requests
-
-**"What Power Queries are in this file?"**
-→ excel_powerquery(list)
-
-**"Show me all DAX measures"**
-→ excel_datamodel(list-measures)
-
-**"What sheets exist?"**
-→ excel_worksheet(list)
-
-**"What connections are available?"**
-→ excel_connection(list)
-
-**"Are there any active batch sessions?"**
-→ list_excel_batches
-
-## Edge Case Interpretations
-
-**"Delete all data"**
-→ excel_range(clear-contents) NOT clear-all (preserve formatting)
-
-**"Get data from A1"**
-→ Remember: Returns [[value]] not value
-→ Extract with result.values[0][0] if needed
-
-**"Hide this sheet from users"**
-→ excel_worksheet(very-hide) for strong protection
-→ excel_worksheet(hide) for normal hiding
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelConnectionPrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelConnectionPrompts.cs
deleted file mode 100644
index 340bfaa2..00000000
--- a/src/ExcelMcp.McpServer/Prompts/ExcelConnectionPrompts.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-using ModelContextProtocol.Server;
-
-namespace Sbroenne.ExcelMcp.McpServer.Prompts;
-
-///
-/// Quick reference prompt for Excel connection types and COM API limitations.
-///
-[McpServerPromptType]
-public static class ExcelConnectionPrompts
-{
- ///
- /// Quick reference for Excel connection types and critical COM API limitations.
- ///
- [McpServerPrompt(Name = "excel_connection_reference")]
- [Description("Quick reference: Excel connection types, which ones work via COM API, and critical limitations")]
- public static ChatMessage ConnectionReference()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("excel_connection.md"));
- }
-}
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelElicitationPrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelElicitationPrompts.cs
index f666a37a..19493d84 100644
--- a/src/ExcelMcp.McpServer/Prompts/ExcelElicitationPrompts.cs
+++ b/src/ExcelMcp.McpServer/Prompts/ExcelElicitationPrompts.cs
@@ -17,11 +17,4 @@ public static ChatMessage DataValidationChecklist()
{
return new ChatMessage(ChatRole.User, MarkdownLoader.LoadElicitation("data_validation.md"));
}
-
- [McpServerPrompt(Name = "excel_troubleshooting_guide")]
- [Description("Common Excel MCP server issues and solutions")]
- public static ChatMessage TroubleshootingGuide()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("server_quirks.md"));
- }
}
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelNamedRangePrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelNamedRangePrompts.cs
deleted file mode 100644
index 68113942..00000000
--- a/src/ExcelMcp.McpServer/Prompts/ExcelNamedRangePrompts.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-using ModelContextProtocol.Server;
-
-namespace Sbroenne.ExcelMcp.McpServer.Prompts;
-
-///
-/// MCP prompts for Excel parameter (named range) management.
-///
-[McpServerPromptType]
-public static class ExcelNamedRangePrompts
-{
- ///
- /// Guide for efficient parameter creation using bulk operations.
- ///
- [McpServerPrompt(Name = "excel_namedrange_bulk_guide")]
- [Description("Guide for creating multiple Excel parameters efficiently using bulk operations")]
- public static ChatMessage NamedRangeBulkGuide()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("excel_namedrange.md"));
- }
-}
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelRangePrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelRangePrompts.cs
deleted file mode 100644
index 05365c62..00000000
--- a/src/ExcelMcp.McpServer/Prompts/ExcelRangePrompts.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-using ModelContextProtocol.Server;
-
-namespace Sbroenne.ExcelMcp.McpServer.Prompts;
-
-///
-/// MCP prompts for Excel range operations - values, formulas, formatting, validation.
-///
-[McpServerPromptType]
-public static class ExcelRangePrompts
-{
- ///
- /// Guide for formatting and styling Excel ranges.
- ///
- [McpServerPrompt(Name = "excel_range_formatting_guide")]
- [Description("Best practices for formatting and styling Excel ranges")]
- public static ChatMessage FormattingGuide()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("excel_range.md"));
- }
-
- ///
- /// Guide for data validation rules in Excel ranges.
- ///
- [McpServerPrompt(Name = "excel_range_validation_guide")]
- [Description("Guide for adding data validation rules to Excel ranges")]
- public static ChatMessage ValidationGuide()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadElicitation("data_validation.md"));
- }
-
- ///
- /// Complete workflow combining values, formulas, formatting, and validation.
- ///
- [McpServerPrompt(Name = "excel_range_complete_workflow")]
- [Description("Complete workflow: values → formulas → formatting → validation")]
- public static ChatMessage CompleteWorkflow()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("excel_range.md"));
- }
-}
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelScenarioPrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelScenarioPrompts.cs
deleted file mode 100644
index 722bf806..00000000
--- a/src/ExcelMcp.McpServer/Prompts/ExcelScenarioPrompts.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-using ModelContextProtocol.Server;
-
-namespace Sbroenne.ExcelMcp.McpServer.Prompts;
-
-///
-/// MCP prompts for common Excel workflow scenarios.
-/// Provides step-by-step templates for frequent tasks.
-///
-[McpServerPromptType]
-public static class ExcelScenarioPrompts
-{
- [McpServerPrompt(Name = "excel_build_financial_report")]
- [Description("Step-by-step guide to build a formatted financial report with formulas")]
- public static ChatMessage BuildFinancialReport(
- [Description("Report title (optional)")] string? reportTitle = null,
- [Description("Number of months (default: 12)")] int? months = null)
- {
- var monthCount = months ?? 12;
- var title = reportTitle ?? "Monthly Revenue Report";
-
- return new ChatMessage(ChatRole.User, $@"
-# BUILD FINANCIAL REPORT: {title}
-
-Complete workflow for creating a professional financial report with formulas and formatting.
-
-**CRITICAL: Keep session open until ALL steps complete - do NOT close prematurely**
-
-## RECOMMENDED WORKFLOW:
-
-1. excel_file(action: 'open', excelPath: 'FinancialReport.xlsx')
- \u2192 Returns sessionId (use for ALL remaining operations)
-2. excel_worksheet(action: 'create', sheetName: 'Report', sessionId: '')
-3. excel_range(action: 'set-values', rangeAddress: 'A1:D1', values: [['Month', 'Revenue', 'Expenses', 'Profit']], sessionId: '')
-4. excel_range(action: 'format-range', rangeAddress: 'A1:D1', bold: true, fillColor: '#4472C4', sessionId: '')
-5. excel_range(action: 'set-formulas', rangeAddress: 'D2:D{monthCount + 1}', formulas: [['=B2-C2'], ...], sessionId: '')
-6. excel_range(action: 'set-number-format', rangeAddress: 'B2:D{monthCount + 1}', formatCode: '$#,##0', sessionId: '')
-7. excel_file(action: 'close', save: true, sessionId: '')
- \u2192 ONLY close when report is complete
-
-RESULT: Professional formatted report with {monthCount} months of data
-");
- }
-
- [McpServerPrompt(Name = "excel_multi_query_import")]
- [Description("Efficiently import multiple Power Queries to Data Model for DAX")]
- public static ChatMessage MultiQueryImport(
- [Description("Number of queries to import")] int? queryCount = null)
- {
- var count = queryCount ?? 4;
-
- return new ChatMessage(ChatRole.User, $@"
-# IMPORT {count} POWER QUERIES TO DATA MODEL
-
-Complete workflow for importing multiple queries and preparing for DAX analysis.
-
-**CRITICAL: Keep session open across ALL query imports - do NOT close between operations**
-
-## RECOMMENDED WORKFLOW:
-
-1. excel_file(action: 'open', excelPath: 'Analytics.xlsx')
- \u2192 Returns sessionId (use for ALL remaining operations)
-2. For each query:
- - excel_powerquery(action: 'create', queryName: '', mCode: '', loadDestination: 'data-model', sessionId: '')
-3. excel_file(action: 'close', save: true, sessionId: '')
- \u2192 ONLY close after ALL queries imported
-
-KEY: Use loadDestination: 'data-model' for direct Power Pivot loading
-RESULT: {count} queries loaded and ready for DAX measures
-");
- }
-
- [McpServerPrompt(Name = "excel_build_data_entry_form")]
- [Description("Build a data entry form with dropdown validation and formatting")]
- public static ChatMessage BuildDataEntryForm()
- {
- return new ChatMessage(ChatRole.User, @"
-# BUILD DATA ENTRY FORM WITH VALIDATION
-
-Create professional data entry form with dropdowns, date validation, and formatted layout.
-
-**CRITICAL: Keep session open until form is complete - do NOT close between operations**
-
-WORKFLOW:
-1. excel_file(action: 'open', excelPath: 'DataEntryForm.xlsx')
- \u2192 Returns sessionId (use for ALL remaining operations)
-2. excel_worksheet(action: 'create', sheetName: 'Employee Form', sessionId: '')
-3. excel_range(action: 'set-values', values: [['Employee ID', 'Name', 'Department', 'Status', 'Hire Date']], sessionId: '')
-4. excel_range(action: 'format-range', rangeAddress: 'A1:E1', bold: true, fillColor: '#D9E1F2', sessionId: '')
-5. excel_range(action: 'validate-range', rangeAddress: 'C2:C100', validationType: 'list', validationFormula1: 'IT,HR,Finance,Operations', sessionId: '')
-6. excel_range(action: 'validate-range', rangeAddress: 'D2:D100', validationType: 'list', validationFormula1: 'Active,Inactive,Leave', sessionId: '')
-7. excel_range(action: 'validate-range', rangeAddress: 'E2:E100', validationType: 'date', sessionId: '')
-8. excel_file(action: 'close', save: true, sessionId: '')
- \u2192 ONLY close when form is complete
-
-RESULT: Professional form with validation, dropdowns, and formatting
-");
- }
-
-
- [McpServerPrompt(Name = "excel_build_analytics_workbook")]
- [Description("Complete workflow: Build analytics workbook with Power Query, Data Model, DAX measures")]
- public static ChatMessage BuildAnalyticsWorkbook()
- {
- return new ChatMessage(ChatRole.User, @"
-# BUILD COMPLETE ANALYTICS WORKBOOK
-
-End-to-end: Import data → Build Data Model → Create DAX measures → Add PivotTable
-
-**CRITICAL: Keep session open across ALL steps - do NOT close between operations**
-
-WORKFLOW:
-1. excel_file(action: 'open', excelPath: 'Analytics.xlsx')
- → Returns sessionId (use for ALL remaining operations)
-2. Import 4 queries with loadDestination: 'data-model' (Sales, Products, Customers, Calendar)
- - excel_powerquery(action: 'create', queryName: 'Sales', mCode: '', loadDestination: 'data-model', sessionId: '')
- - ... (repeat for Products, Customers, Calendar)
-3. Create 3 relationships
- - excel_datamodel(action: 'create-relationship', fromTable: 'Sales', fromColumn: 'ProductID', toTable: 'Products', toColumn: 'ProductID', sessionId: '')
- - excel_datamodel(action: 'create-relationship', fromTable: 'Sales', fromColumn: 'CustomerID', toTable: 'Customers', toColumn: 'CustomerID', sessionId: '')
- - excel_datamodel(action: 'create-relationship', fromTable: 'Sales', fromColumn: 'DateID', toTable: 'Calendar', toColumn: 'DateID', sessionId: '')
-4. Create 4 DAX measures
- - excel_datamodel(action: 'create-measure', tableName: 'Measures', measureName: 'Total Revenue', daxFormula: 'SUM(Sales[Amount])', sessionId: '')
- - ... (repeat for other measures)
-5. excel_pivottable(action: 'create-from-datamodel', dataModelTableName: 'Sales', destinationSheet: 'PivotTable', destinationCell: 'A1', sessionId: '')
-6. excel_file(action: 'close', save: true, sessionId: '')
- → ONLY close when analytics workbook is complete
-
-RESULT: 4 data sources, 3 relationships, 4 DAX measures, 1 PivotTable
-");
- }
-}
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelToolSelectionPrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelToolSelectionPrompts.cs
deleted file mode 100644
index 9ebf6558..00000000
--- a/src/ExcelMcp.McpServer/Prompts/ExcelToolSelectionPrompts.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-using ModelContextProtocol.Server;
-
-namespace Sbroenne.ExcelMcp.McpServer.Prompts;
-
-///
-/// MCP prompt for helping LLMs choose the right Excel tool for the task.
-///
-[McpServerPromptType]
-public static class ExcelToolSelectionPrompts
-{
- ///
- /// Comprehensive guide for selecting the appropriate Excel tool.
- ///
- [McpServerPrompt(Name = "excel_tool_selection_guide")]
- [Description("Guide for choosing the right Excel tool based on the user's request")]
- public static ChatMessage ToolSelectionGuide()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("tool_selection_guide.md"));
- }
-}
diff --git a/src/ExcelMcp.McpServer/Prompts/ExcelVbaPrompts.cs b/src/ExcelMcp.McpServer/Prompts/ExcelVbaPrompts.cs
deleted file mode 100644
index 3db3ad2a..00000000
--- a/src/ExcelMcp.McpServer/Prompts/ExcelVbaPrompts.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.ComponentModel;
-using Microsoft.Extensions.AI;
-using ModelContextProtocol.Server;
-
-namespace Sbroenne.ExcelMcp.McpServer.Prompts;
-
-///
-/// MCP prompts for Excel VBA macro management.
-///
-[McpServerPromptType]
-public static class ExcelVbaPrompts
-{
- ///
- /// Guide for VBA macro version control and automation workflows.
- ///
- [McpServerPrompt(Name = "excel_vba_version_control_guide")]
- [Description("Guide for managing VBA macros with version control and automation")]
- public static ChatMessage VbaVersionControlGuide()
- {
- return new ChatMessage(ChatRole.User, MarkdownLoader.LoadPrompt("excel_vba.md"));
- }
-}
diff --git a/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs b/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs
index 01db9a3f..bca25d1e 100644
--- a/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs
+++ b/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetry.cs
@@ -12,27 +12,6 @@ namespace Sbroenne.ExcelMcp.McpServer.Telemetry;
///
public static class ExcelMcpTelemetry
{
- ///
- /// Environment variable to opt-out of telemetry.
- /// Set to "true" or "1" to disable telemetry.
- ///
- public const string OptOutEnvironmentVariable = "EXCELMCP_TELEMETRY_OPTOUT";
-
- ///
- /// Environment variable to enable debug mode (console output instead of Azure).
- /// Set to "true" or "1" for local testing without Azure resources.
- ///
- public const string DebugTelemetryEnvironmentVariable = "EXCELMCP_DEBUG_TELEMETRY";
-
- ///
- /// Application Insights connection string (embedded at build time).
- ///
- ///
- /// This value is replaced during CI/CD build from the APPINSIGHTS_CONNECTION_STRING secret.
- /// Format: InstrumentationKey=xxx;IngestionEndpoint=https://xxx.in.applicationinsights.azure.com/;...
- ///
- private const string ConnectionString = "__APPINSIGHTS_CONNECTION_STRING__";
-
///
/// Unique session ID for correlating telemetry within a single MCP server process.
/// Changes each time the MCP server starts.
@@ -58,31 +37,33 @@ public static class ExcelMcpTelemetry
///
internal static void SetTelemetryClient(TelemetryClient client)
{
+ Console.Error.WriteLine($"[TELEMETRY DIAG] SetTelemetryClient called with client={(client == null ? "NULL" : "instance")}");
_telemetryClient = client;
}
- private static bool? _isEnabled;
-
///
- /// Gets whether telemetry is enabled (not opted out and either debug mode or connection string available).
+ /// Flushes any buffered telemetry to Application Insights.
+ /// CRITICAL: Must be called before application exits to ensure telemetry is not lost.
+ /// Application Insights SDK buffers telemetry and sends in batches - without explicit flush,
+ /// short-lived processes like MCP servers may terminate before telemetry is transmitted.
///
- public static bool IsEnabled
+ public static void Flush()
{
- get
+ Console.Error.WriteLine($"[TELEMETRY DIAG] Flush called, client={((_telemetryClient == null) ? "NULL" : "SET")}");
+ if (_telemetryClient == null) return;
+
+ try
{
- _isEnabled ??= !IsOptedOut() && (IsDebugMode() || !string.IsNullOrEmpty(GetConnectionString()));
- return _isEnabled.Value;
+ // Flush with timeout to avoid hanging on shutdown
+ // 5 seconds is typically sufficient for small batches
+ _telemetryClient.FlushAsync(CancellationToken.None).Wait(TimeSpan.FromSeconds(5));
+ Console.Error.WriteLine($"[TELEMETRY DIAG] Flush completed successfully");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"[TELEMETRY DIAG] Flush failed: {ex.Message}");
+ // Don't let telemetry flush failure crash the application
}
- }
-
- ///
- /// Checks if debug telemetry mode is enabled (console output for local testing).
- ///
- public static bool IsDebugMode()
- {
- var debug = Environment.GetEnvironmentVariable(DebugTelemetryEnvironmentVariable);
- return string.Equals(debug, "true", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(debug, "1", StringComparison.Ordinal);
}
///
@@ -90,24 +71,23 @@ public static bool IsDebugMode()
///
public static string? GetConnectionString()
{
- // Connection string is embedded at build time via CI/CD
- // Returns null if placeholder wasn't replaced (local dev builds)
- return ConnectionString.StartsWith("__", StringComparison.Ordinal) ? null : ConnectionString;
- }
-
- ///
- /// Checks if user has opted out of telemetry via environment variable.
- ///
- public static bool IsOptedOut()
- {
- var optOut = Environment.GetEnvironmentVariable(OptOutEnvironmentVariable);
- return string.Equals(optOut, "true", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(optOut, "1", StringComparison.OrdinalIgnoreCase);
+ // Connection string is embedded at build time from Directory.Build.props.user
+ // Returns null if not set (placeholder value starts with __)
+ if (string.IsNullOrEmpty(TelemetryConfig.ConnectionString) ||
+ TelemetryConfig.ConnectionString.StartsWith("__", StringComparison.Ordinal))
+ {
+ Console.Error.WriteLine($"[TELEMETRY DIAG] GetConnectionString returning null. Raw value: '{TelemetryConfig.ConnectionString ?? "(null)"}'");
+ return null;
+ }
+ Console.Error.WriteLine($"[TELEMETRY DIAG] GetConnectionString returning valid connection string (length={TelemetryConfig.ConnectionString.Length})");
+ return TelemetryConfig.ConnectionString;
}
///
/// Tracks a tool invocation with usage metrics.
- /// Sends Application Insights Custom Event for Users/Sessions analytics.
+ /// Sends Application Insights Request and PageView telemetry.
+ /// - Request: Populates Performance, Failures, Users, Sessions blades
+ /// - PageView: Enables User Flows blade (shows tool usage patterns)
///
/// The MCP tool name (e.g., "excel_range")
/// The action performed (e.g., "get-values")
@@ -115,74 +95,47 @@ public static bool IsOptedOut()
/// Whether the operation succeeded
public static void TrackToolInvocation(string toolName, string action, long durationMs, bool success)
{
- if (!IsEnabled) return;
+ Console.Error.WriteLine($"[TELEMETRY DIAG] TrackToolInvocation called: {toolName}/{action}, client={((_telemetryClient == null) ? "NULL" : "SET")}");
+ if (_telemetryClient == null) return;
- // Debug mode: write to stderr
- if (IsDebugMode())
- {
- Console.Error.WriteLine($"[Telemetry] ToolInvocation: {toolName}.{action} - {(success ? "Success" : "Failed")} ({durationMs}ms)");
- }
+ var operationName = $"{toolName}/{action}";
+ var startTime = DateTimeOffset.UtcNow.AddMilliseconds(-durationMs);
+ var duration = TimeSpan.FromMilliseconds(durationMs);
- // Send Application Insights Custom Event (populates customEvents table)
- if (_telemetryClient != null)
+ // Request telemetry: Performance, Failures, Users, Sessions
+ _telemetryClient.TrackRequest(operationName, startTime, duration, success ? "200" : "500", success);
+
+ // PageView telemetry: Enables User Flows blade
+ // Must include duration for proper User Flows visualization
+ var pageView = new Microsoft.ApplicationInsights.DataContracts.PageViewTelemetry(operationName)
{
- var properties = new Dictionary
- {
- { "ToolName", toolName },
- { "Action", action },
- { "Success", success.ToString() },
- { "AppVersion", GetVersion() }
- };
-
- var metrics = new Dictionary
- {
- { "DurationMs", durationMs }
- };
-
- _telemetryClient.TrackEvent("ToolInvocation", properties, metrics);
- }
+ Timestamp = startTime,
+ Duration = duration
+ };
+ _telemetryClient.TrackPageView(pageView);
+ Console.Error.WriteLine($"[TELEMETRY DIAG] TrackRequest + TrackPageView completed for {operationName}");
}
///
/// Tracks an unhandled exception.
/// Only call this for exceptions that escape all catch blocks (true bugs/crashes).
- /// Sends Application Insights exception and Custom Event.
///
/// The unhandled exception
/// Source of the exception (e.g., "AppDomain.UnhandledException")
public static void TrackUnhandledException(Exception exception, string source)
{
- if (!IsEnabled || exception == null) return;
+ if (_telemetryClient == null || exception == null) return;
// Redact sensitive data from exception
- var (type, message, _) = SensitiveDataRedactor.RedactException(exception);
+ var (type, _, _) = SensitiveDataRedactor.RedactException(exception);
- // Debug mode: write to stderr
- if (IsDebugMode())
+ // Track as exception in Application Insights (for Failures blade)
+ _telemetryClient.TrackException(exception, new Dictionary
{
- Console.Error.WriteLine($"[Telemetry] UnhandledException: {type} - {message} (Source: {source})");
- }
-
- // Send Application Insights telemetry
- if (_telemetryClient != null)
- {
- // Track as exception in Application Insights (for Failures blade)
- _telemetryClient.TrackException(exception, new Dictionary
- {
- { "Source", source },
- { "ExceptionType", type },
- { "AppVersion", GetVersion() }
- });
-
- // Also track as Custom Event (for Users/Sessions analytics)
- _telemetryClient.TrackEvent("UnhandledException", new Dictionary
- {
- { "Source", source },
- { "ExceptionType", type },
- { "Message", message ?? "Unknown" },
- { "AppVersion", GetVersion() }
- });
- }
+ { "Source", source },
+ { "ExceptionType", type },
+ { "AppVersion", GetVersion() }
+ });
}
///
diff --git a/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetryInitializer.cs b/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetryInitializer.cs
index efe64aff..5bbc3ac7 100644
--- a/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetryInitializer.cs
+++ b/src/ExcelMcp.McpServer/Telemetry/ExcelMcpTelemetryInitializer.cs
@@ -1,19 +1,23 @@
// Copyright (c) Sbroenne. All rights reserved.
// Licensed under the MIT License.
+using System.Reflection;
+
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
namespace Sbroenne.ExcelMcp.McpServer.Telemetry;
///
-/// Telemetry initializer that sets User.Id and Session.Id for Application Insights.
-/// This enables the Users and Sessions blades in the Azure Portal.
+/// Telemetry initializer that sets User.Id, Session.Id, and Component.Version for Application Insights.
+/// This enables the Users and Sessions blades in the Azure Portal and ensures correct version reporting.
///
public sealed class ExcelMcpTelemetryInitializer : ITelemetryInitializer
{
private readonly string _userId;
private readonly string _sessionId;
+ private readonly string _version;
+ private readonly string _roleInstance;
///
/// Initializes a new instance of the class.
@@ -22,10 +26,12 @@ public ExcelMcpTelemetryInitializer()
{
_userId = ExcelMcpTelemetry.UserId;
_sessionId = ExcelMcpTelemetry.SessionId;
+ _version = GetVersion();
+ _roleInstance = GenerateAnonymousRoleInstance();
}
///
- /// Initializes the telemetry item with user and session context.
+ /// Initializes the telemetry item with user, session, and version context.
///
/// The telemetry item to initialize.
public void Initialize(ITelemetry telemetry)
@@ -47,5 +53,33 @@ public void Initialize(ITelemetry telemetry)
{
telemetry.Context.Cloud.RoleName = "ExcelMcp.McpServer";
}
+
+ // Set role instance to anonymized value (instead of machine name)
+ telemetry.Context.Cloud.RoleInstance = _roleInstance;
+
+ // Set version explicitly - ALWAYS override SDK auto-detection
+ // SDK picks up Excel COM version (15.0.0.0) instead of our assembly version
+ telemetry.Context.Component.Version = _version;
+ }
+
+ ///
+ /// Generates an anonymous role instance identifier based on machine identity.
+ /// Uses the same hash as UserId but with a different prefix for clarity.
+ ///
+ private static string GenerateAnonymousRoleInstance()
+ {
+ // Reuse the anonymous user ID (already a hash of machine identity)
+ return $"instance-{ExcelMcpTelemetry.UserId[..8]}";
+ }
+
+ ///
+ /// Gets the application version from assembly metadata.
+ ///
+ private static string GetVersion()
+ {
+ return Assembly.GetExecutingAssembly()
+ .GetCustomAttribute()?.InformationalVersion
+ ?? Assembly.GetExecutingAssembly().GetName().Version?.ToString()
+ ?? "1.0.0";
}
}
diff --git a/src/ExcelMcp.McpServer/Tools/ExcelNamedRangeTool.cs b/src/ExcelMcp.McpServer/Tools/ExcelNamedRangeTool.cs
index a5b70ccc..c0958c6a 100644
--- a/src/ExcelMcp.McpServer/Tools/ExcelNamedRangeTool.cs
+++ b/src/ExcelMcp.McpServer/Tools/ExcelNamedRangeTool.cs
@@ -17,7 +17,16 @@ public static class ExcelNamedRangeTool
/// Manage Excel parameters (named ranges) - configuration values and reusable references
///
[McpServerTool(Name = "excel_namedrange")]
- [Description(@"Manage Excel named ranges")]
+ [Description(@"Manage Excel named ranges - named cell references for reusable formulas and parameters.
+
+CREATE/UPDATE:
+- value is a cell reference (e.g., 'Sheet1!A1' or 'Sheet1!$A$1:$B$10')
+- Use $ for absolute references that won't shift when copied
+
+WRITE:
+- value is the actual data to store in the named range's cell(s)
+
+TIP: Use excel_range with rangeAddress=namedRangeName for bulk data operations on named ranges.")]
public static string ExcelParameter(
[Required]
[Description("Action to perform (enum displayed as dropdown in MCP clients)")]
diff --git a/src/ExcelMcp.McpServer/Tools/ExcelRangeTool.cs b/src/ExcelMcp.McpServer/Tools/ExcelRangeTool.cs
index 271f0b07..d8978f1f 100644
--- a/src/ExcelMcp.McpServer/Tools/ExcelRangeTool.cs
+++ b/src/ExcelMcp.McpServer/Tools/ExcelRangeTool.cs
@@ -24,8 +24,11 @@ public static class ExcelRangeTool
DATA FORMAT:
- Values/formulas: JSON 2D arrays [[row1col1, row1col2], [row2col1, row2col2]]
-- Example single cell: [[100]] or [['=SUM(A:A)']]
-- Example range: [[1,2,3], [4,5,6], [7,8,9]]
+- Single cell returns [[value]] (always 2D, never scalar)
+
+NAMED RANGES:
+- Can use named range name instead of rangeAddress (e.g., 'SalesData' instead of 'A1:D10')
+- When using named range, leave sheetName empty
")]
public static string ExcelRange(
[Required]
diff --git a/src/ExcelMcp.McpServer/Tools/ExcelWorksheetTool.cs b/src/ExcelMcp.McpServer/Tools/ExcelWorksheetTool.cs
index be107f8c..5556f5e0 100644
--- a/src/ExcelMcp.McpServer/Tools/ExcelWorksheetTool.cs
+++ b/src/ExcelMcp.McpServer/Tools/ExcelWorksheetTool.cs
@@ -25,17 +25,10 @@ public static class ExcelWorksheetTool
[McpServerTool(Name = "excel_worksheet")]
[Description(@"Manage Excel worksheets: lifecycle, tab colors, visibility.
-REQUIRED WORKFLOW:
-- Use excel_file(action: 'open') first to get a sessionId
-- Pass sessionId to all worksheet actions
-- Use excel_file(action: 'save') to persist changes
-- Use excel_file(action: 'close') to end the session (does NOT save)
-
CROSS-WORKBOOK OPERATIONS (copy-to-workbook, move-to-workbook):
- Copy or move sheets BETWEEN different Excel files
- Requires TWO sessionIds: sourceSessionId + targetSessionId
- Opens both workbooks in same Excel instance automatically
-- Example: Copy 'Sales' from Q1.xlsx to Q2.xlsx using both session IDs
TAB COLORS (set-tab-color):
- RGB values: 0-255 for red, green, blue components
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index 42fd10fd..f544501a 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -2,7 +2,5 @@
true
-
- $(MSBuildThisFileDirectory)test.runsettings
diff --git a/tests/ExcelMcp.McpServer.Tests/ExcelMcp.McpServer.Tests.csproj b/tests/ExcelMcp.McpServer.Tests/ExcelMcp.McpServer.Tests.csproj
index b101a61b..c6201af0 100644
--- a/tests/ExcelMcp.McpServer.Tests/ExcelMcp.McpServer.Tests.csproj
+++ b/tests/ExcelMcp.McpServer.Tests/ExcelMcp.McpServer.Tests.csproj
@@ -16,6 +16,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/ExcelMcp.McpServer.Tests/Integration/McpServerIntegrationTests.cs b/tests/ExcelMcp.McpServer.Tests/Integration/McpServerIntegrationTests.cs
new file mode 100644
index 00000000..1fa543b4
--- /dev/null
+++ b/tests/ExcelMcp.McpServer.Tests/Integration/McpServerIntegrationTests.cs
@@ -0,0 +1,371 @@
+// Copyright (c) Sbroenne. All rights reserved.
+// Licensed under the MIT License.
+
+using System.IO.Pipelines;
+using Microsoft.ApplicationInsights;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using Sbroenne.ExcelMcp.McpServer.Telemetry;
+using Sbroenne.ExcelMcp.McpServer.Tools;
+using Xunit;
+using Xunit.Abstractions;
+
+// Avoid namespace conflict: McpServer is both a type and namespace
+using Server = ModelContextProtocol.Server;
+
+namespace Sbroenne.ExcelMcp.McpServer.Tests.Integration;
+
+///
+/// Integration tests that exercise the full MCP protocol using in-memory transport.
+/// These tests use the official MCP SDK client to connect to our server, ensuring:
+/// - DI pipeline is correctly configured
+/// - Tool discovery via WithToolsFromAssembly() works
+/// - Tool schemas are correctly generated
+/// - Tools execute properly through the MCP protocol
+///
+/// This is the CORRECT way to test MCP servers - using the SDK's client to verify
+/// the actual protocol behavior, not reflection or direct method calls.
+///
+[Trait("Category", "Integration")]
+[Trait("Speed", "Fast")]
+[Trait("Layer", "McpServer")]
+[Trait("Feature", "McpProtocol")]
+public class McpServerIntegrationTests(ITestOutputHelper output) : IAsyncLifetime, IAsyncDisposable
+{
+ private readonly Pipe _clientToServerPipe = new();
+ private readonly Pipe _serverToClientPipe = new();
+ private readonly CancellationTokenSource _cts = new();
+ private Server.McpServer? _server;
+ private McpClient? _client;
+ private IServiceProvider? _serviceProvider;
+ private Task? _serverTask;
+
+ ///
+ /// Expected tool names from our assembly - the source of truth.
+ ///
+ private static readonly HashSet ExpectedToolNames =
+ [
+ "excel_chart",
+ "excel_conditionalformat",
+ "excel_connection",
+ "excel_datamodel",
+ "excel_file",
+ "excel_namedrange",
+ "excel_pivottable",
+ "excel_powerquery",
+ "excel_range",
+ "excel_table",
+ "excel_vba",
+ "excel_worksheet"
+ ];
+
+ ///
+ /// Setup: Create MCP server with DI and connect client via in-memory pipes.
+ /// This exercises the exact same code path as Program.cs.
+ ///
+ public async Task InitializeAsync()
+ {
+ // Build the server with DI - same pattern as Program.cs
+ var services = new ServiceCollection();
+ services.AddLogging(builder => builder.AddDebug().SetMinimumLevel(LogLevel.Debug));
+
+ // Configure telemetry (disabled for tests)
+ services.AddApplicationInsightsTelemetryWorkerService(options =>
+ {
+ options.ConnectionString = null;
+ options.EnableHeartbeat = false;
+ options.EnableAdaptiveSampling = false;
+ options.EnableQuickPulseMetricStream = false;
+ options.EnablePerformanceCounterCollectionModule = false;
+ options.EnableEventCounterCollectionModule = false;
+ options.EnableDependencyTrackingTelemetryModule = false;
+ });
+ services.AddSingleton();
+
+ // Add MCP server with tools (same as Program.cs) using stream transport for testing
+ services
+ .AddMcpServer(options =>
+ {
+ options.ServerInfo = new() { Name = "ExcelMcp-Test", Version = "1.0.0" };
+ options.ServerInstructions = "Test server for integration tests";
+ })
+ .WithStreamServerTransport(
+ _clientToServerPipe.Reader.AsStream(),
+ _serverToClientPipe.Writer.AsStream())
+ .WithToolsFromAssembly(typeof(ExcelFileTool).Assembly);
+
+ _serviceProvider = services.BuildServiceProvider(validateScopes: true);
+
+ // Get the server and start it
+ _server = _serviceProvider.GetRequiredService();
+ _serverTask = _server.RunAsync(_cts.Token);
+
+ // Create client connected to the server via pipes
+ _client = await McpClient.CreateAsync(
+ new StreamClientTransport(
+ serverInput: _clientToServerPipe.Writer.AsStream(),
+ serverOutput: _serverToClientPipe.Reader.AsStream()),
+ clientOptions: new McpClientOptions
+ {
+ ClientInfo = new() { Name = "TestClient", Version = "1.0.0" }
+ },
+ cancellationToken: _cts.Token);
+
+ output.WriteLine($"✓ Connected to server: {_client.ServerInfo?.Name} v{_client.ServerInfo?.Version}");
+ }
+
+ public async Task DisposeAsync()
+ {
+ await DisposeAsyncCore();
+ }
+
+ // Explicit IAsyncDisposable implementation to satisfy CA1001 analyzer
+ async ValueTask IAsyncDisposable.DisposeAsync()
+ {
+ await DisposeAsyncCore();
+ GC.SuppressFinalize(this);
+ }
+
+ private async Task DisposeAsyncCore()
+ {
+ await _cts.CancelAsync();
+
+ _clientToServerPipe.Writer.Complete();
+ _serverToClientPipe.Writer.Complete();
+
+ if (_client != null)
+ {
+ await _client.DisposeAsync();
+ }
+
+ if (_serverTask != null)
+ {
+ try
+ {
+ await _serverTask;
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected during shutdown
+ }
+ }
+
+ if (_serviceProvider is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync();
+ }
+ else if (_serviceProvider is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ _cts.Dispose();
+ }
+
+ ///
+ /// Tests that all 12 expected tools are discoverable via the MCP protocol.
+ /// This is THE definitive test - it uses client.ListToolsAsync() which exercises:
+ /// - DI pipeline
+ /// - WithToolsFromAssembly() discovery
+ /// - MCP protocol serialization
+ /// - Tool schema generation
+ ///
+ [Fact]
+ public async Task ListTools_ReturnsAll12ExpectedTools()
+ {
+ output.WriteLine("=== TOOL DISCOVERY VIA MCP PROTOCOL ===\n");
+
+ // Act - Use the REAL MCP protocol to list tools
+ var tools = await _client!.ListToolsAsync(cancellationToken: _cts.Token);
+
+ // Assert - Verify count
+ output.WriteLine($"Discovered {tools.Count} tools via MCP protocol:\n");
+
+ foreach (var tool in tools.OrderBy(t => t.Name))
+ {
+ var descPreview = tool.Description?.Length > 60 ? tool.Description[..60] + "..." : tool.Description;
+ output.WriteLine($" • {tool.Name}: {descPreview}");
+ }
+
+ Assert.Equal(ExpectedToolNames.Count, tools.Count);
+
+ // Verify all expected tools are present
+ var actualToolNames = tools.Select(t => t.Name).ToHashSet();
+
+ var missingTools = ExpectedToolNames.Except(actualToolNames).ToList();
+ if (missingTools.Count > 0)
+ {
+ output.WriteLine($"\n❌ Missing tools: {string.Join(", ", missingTools)}");
+ }
+ Assert.Empty(missingTools);
+
+ var unexpectedTools = actualToolNames.Except(ExpectedToolNames).ToList();
+ if (unexpectedTools.Count > 0)
+ {
+ output.WriteLine($"\n❌ Unexpected tools: {string.Join(", ", unexpectedTools)}");
+ }
+ Assert.Empty(unexpectedTools);
+
+ output.WriteLine($"\n✓ All {ExpectedToolNames.Count} tools discovered successfully via MCP protocol");
+ }
+
+ ///
+ /// Tests that each tool has proper schema (parameters, descriptions).
+ ///
+ [Fact]
+ public async Task ListTools_AllToolsHaveValidSchema()
+ {
+ output.WriteLine("=== TOOL SCHEMA VALIDATION ===\n");
+
+ var tools = await _client!.ListToolsAsync(cancellationToken: _cts.Token);
+
+ foreach (var tool in tools)
+ {
+ // Every tool must have a name
+ Assert.False(string.IsNullOrEmpty(tool.Name), "Tool has empty name");
+
+ // Every tool should have a description
+ Assert.False(string.IsNullOrEmpty(tool.Description), $"Tool {tool.Name} has no description");
+
+ // McpClientTool implements AIFunction which has Parameters property
+ // The SDK generates schema from tool methods
+
+ output.WriteLine($"✓ {tool.Name}: Has description ({tool.Description?.Length} chars)");
+ }
+
+ output.WriteLine($"\n✓ All {tools.Count} tools have valid schemas");
+ }
+
+ ///
+ /// Tests that excel_file tool's Test action works via MCP protocol.
+ /// This exercises the complete tool invocation path.
+ ///
+ [Fact]
+ public async Task CallTool_ExcelFileTest_ReturnsSuccess()
+ {
+ output.WriteLine("=== TOOL INVOCATION VIA MCP PROTOCOL ===\n");
+
+ // Arrange - Test action doesn't require an actual file
+ var arguments = new Dictionary
+ {
+ ["action"] = "Test",
+ ["excelPath"] = "C:\\fake\\test.xlsx"
+ };
+
+ // Act - Call tool via MCP protocol
+ var result = await _client!.CallToolAsync(
+ "excel_file",
+ arguments,
+ cancellationToken: _cts.Token);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotNull(result.Content);
+ Assert.NotEmpty(result.Content);
+
+ // Get text content - need to cast from ContentBlock base class
+ var textBlock = result.Content.OfType().FirstOrDefault();
+ Assert.NotNull(textBlock);
+
+ var textPreview = textBlock.Text.Length > 200 ? textBlock.Text[..200] + "..." : textBlock.Text;
+ output.WriteLine($"Tool response: {textPreview}");
+
+ // The test action should return success
+ Assert.Contains("success", textBlock.Text.ToLowerInvariant());
+
+ output.WriteLine("\n✓ excel_file Test action executed successfully via MCP protocol");
+ }
+
+ ///
+ /// Tests that server information is correctly exposed via MCP protocol.
+ ///
+ [Fact]
+ public async Task ServerInfo_ReturnsCorrectInformation()
+ {
+ output.WriteLine("=== SERVER INFO VIA MCP PROTOCOL ===\n");
+
+ // Act - Server info is available after connection
+ var serverInfo = _client!.ServerInfo;
+ var serverInstructions = _client.ServerInstructions;
+
+ // Assert
+ Assert.NotNull(serverInfo);
+ Assert.Equal("ExcelMcp-Test", serverInfo.Name);
+ Assert.Equal("1.0.0", serverInfo.Version);
+ Assert.Equal("Test server for integration tests", serverInstructions);
+
+ output.WriteLine($"Server Name: {serverInfo.Name}");
+ output.WriteLine($"Server Version: {serverInfo.Version}");
+ output.WriteLine($"Server Instructions: {serverInstructions}");
+
+ output.WriteLine("\n✓ Server info correctly exposed via MCP protocol");
+ await Task.CompletedTask; // Satisfy async requirement
+ }
+
+ ///
+ /// Tests that telemetry services are properly registered in DI.
+ ///
+ [Fact]
+ public void DI_TelemetryServicesRegistered()
+ {
+ output.WriteLine("=== TELEMETRY DI REGISTRATION ===\n");
+
+ Assert.NotNull(_serviceProvider);
+
+ // Act - Verify telemetry services are available
+ var telemetryClient = _serviceProvider.GetService();
+ var telemetryInitializers = _serviceProvider.GetServices().ToList();
+
+ // Assert
+ Assert.NotNull(telemetryClient);
+ Assert.Contains(telemetryInitializers, i => i is ExcelMcpTelemetryInitializer);
+
+ output.WriteLine("✓ TelemetryClient registered");
+ output.WriteLine($"✓ Found {telemetryInitializers.Count} telemetry initializers");
+ output.WriteLine("✓ ExcelMcpTelemetryInitializer present");
+
+ output.WriteLine("\n✓ Telemetry services correctly registered in DI");
+ }
+
+ ///
+ /// Tests that tools can be enumerated lazily (important for large tool sets).
+ ///
+ [Fact]
+ public async Task EnumerateTools_SupportsLazyEnumeration()
+ {
+ output.WriteLine("=== LAZY TOOL ENUMERATION ===\n");
+
+ var toolCount = 0;
+ await foreach (var tool in _client!.EnumerateToolsAsync(cancellationToken: _cts.Token))
+ {
+ toolCount++;
+ output.WriteLine($" Discovered: {tool.Name}");
+ }
+
+ Assert.Equal(ExpectedToolNames.Count, toolCount);
+
+ output.WriteLine($"\n✓ Enumerated {toolCount} tools lazily");
+ }
+
+ ///
+ /// Tests that server capabilities include tools.
+ ///
+ [Fact]
+ public void ServerCapabilities_IncludesTools()
+ {
+ output.WriteLine("=== SERVER CAPABILITIES ===\n");
+
+ var capabilities = _client!.ServerCapabilities;
+
+ Assert.NotNull(capabilities);
+ Assert.NotNull(capabilities.Tools);
+
+ output.WriteLine($"✓ Tools capability: {capabilities.Tools != null}");
+ output.WriteLine($"✓ ListChanged: {capabilities.Tools?.ListChanged}");
+
+ output.WriteLine("\n✓ Server capabilities correctly exposed");
+ }
+}
diff --git a/tests/ExcelMcp.McpServer.Tests/Integration/Models/ActionEnumCompletenessTests.cs b/tests/ExcelMcp.McpServer.Tests/Integration/Models/ActionEnumCompletenessTests.cs
index f16b079d..05266349 100644
--- a/tests/ExcelMcp.McpServer.Tests/Integration/Models/ActionEnumCompletenessTests.cs
+++ b/tests/ExcelMcp.McpServer.Tests/Integration/Models/ActionEnumCompletenessTests.cs
@@ -14,20 +14,14 @@ namespace Sbroenne.ExcelMcp.McpServer.Tests.Integration.Models;
///
/// Uses reflection to automatically discover ALL action enums - no manual maintenance required.
///
+///
[Trait("Category", "Integration")]
[Trait("Speed", "Fast")]
[Trait("Layer", "McpServer")]
[Trait("Feature", "ActionEnums")]
[Trait("RequiresExcel", "false")]
-public class ActionEnumCompletenessTests
+public class ActionEnumCompletenessTests(ITestOutputHelper output)
{
- private readonly ITestOutputHelper _output;
- ///
-
- public ActionEnumCompletenessTests(ITestOutputHelper output)
- {
- _output = output;
- }
///
/// CRITICAL: Discovers ALL *Action enums and verifies every value has a ToActionString() mapping.
@@ -45,10 +39,10 @@ public void AllActionEnums_HaveCompleteToActionStringMappings()
.Where(t => t.IsEnum && t.Name.EndsWith("Action", StringComparison.Ordinal) && t.Namespace == "Sbroenne.ExcelMcp.McpServer.Models")
.ToList();
- _output.WriteLine($"Found {actionEnums.Count} action enums:");
+ output.WriteLine($"Found {actionEnums.Count} action enums:");
foreach (var enumType in actionEnums)
{
- _output.WriteLine($" - {enumType.Name}");
+ output.WriteLine($" - {enumType.Name}");
}
Assert.NotEmpty(actionEnums); // Sanity check
@@ -87,7 +81,7 @@ public void AllActionEnums_HaveCompleteToActionStringMappings()
}
else
{
- _output.WriteLine($" ✅ {enumType.Name}.{enumValue} → '{result}'");
+ output.WriteLine($" ✅ {enumType.Name}.{enumValue} → '{result}'");
}
}
catch (TargetInvocationException ex) when (ex.InnerException is ArgumentException argEx)
@@ -104,7 +98,7 @@ public void AllActionEnums_HaveCompleteToActionStringMappings()
if (failures.Count > 0)
{
var message = $"Enum mapping failures:\n{string.Join("\n", failures)}";
- _output.WriteLine($"\n{message}");
+ output.WriteLine($"\n{message}");
Assert.Fail(message);
}
}
@@ -171,7 +165,7 @@ public void AllActionEnums_NoDuplicateActionStrings()
if (failures.Count > 0)
{
var message = $"Duplicate action string failures:\n{string.Join("\n", failures)}";
- _output.WriteLine($"\n{message}");
+ output.WriteLine($"\n{message}");
Assert.Fail(message);
}
}
@@ -192,17 +186,17 @@ public void AllActionEnums_DocumentedInToolFiles()
.Where(t => t.IsEnum && t.Name.EndsWith("Action", StringComparison.Ordinal) && t.Namespace == "Sbroenne.ExcelMcp.McpServer.Models")
.ToList();
- _output.WriteLine($"\nExpected tool files with switch statements:");
- _output.WriteLine($"Each *Action enum should have corresponding *Tool.cs with exhaustive switch.\n");
+ output.WriteLine($"\nExpected tool files with switch statements:");
+ output.WriteLine($"Each *Action enum should have corresponding *Tool.cs with exhaustive switch.\n");
foreach (var enumType in actionEnums)
{
var toolName = enumType.Name.Replace("Action", "Tool");
- _output.WriteLine($" - {enumType.Name} → Tools/{toolName}.cs");
- _output.WriteLine($" Expected: switch (action.ToActionString()) with all {Enum.GetValues(enumType).Length} cases");
+ output.WriteLine($" - {enumType.Name} → Tools/{toolName}.cs");
+ output.WriteLine($" Expected: switch (action.ToActionString()) with all {Enum.GetValues(enumType).Length} cases");
}
- _output.WriteLine($"\n✅ Compiler enforces exhaustive switches via warning CS8524.");
- _output.WriteLine($"✅ Build with TreatWarningsAsErrors=true ensures no missing cases.");
+ output.WriteLine($"\n✅ Compiler enforces exhaustive switches via warning CS8524.");
+ output.WriteLine($"✅ Build with TreatWarningsAsErrors=true ensures no missing cases.");
}
}
diff --git a/tests/ExcelMcp.McpServer.Tests/Integration/Tools/McpServerSmokeTests.cs b/tests/ExcelMcp.McpServer.Tests/Integration/Tools/McpServerSmokeTests.cs
index acbc6950..5ed329eb 100644
--- a/tests/ExcelMcp.McpServer.Tests/Integration/Tools/McpServerSmokeTests.cs
+++ b/tests/ExcelMcp.McpServer.Tests/Integration/Tools/McpServerSmokeTests.cs
@@ -1,34 +1,55 @@
+// Copyright (c) Sbroenne. All rights reserved.
+// Licensed under the MIT License.
+
+using System.IO.Pipelines;
using System.Text.Json;
-using Sbroenne.ExcelMcp.Core.Commands.Chart;
-using Sbroenne.ExcelMcp.McpServer.Models;
-using Sbroenne.ExcelMcp.McpServer.Tools;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using Sbroenne.ExcelMcp.McpServer.Telemetry;
using Xunit;
using Xunit.Abstractions;
namespace Sbroenne.ExcelMcp.McpServer.Tests.Integration.Tools;
///
-/// Smoke test for MCP Server - Quick validation of core functionality from an LLM perspective.
+/// End-to-end smoke tests for the MCP Server using the official MCP SDK client.
+///
+/// PURPOSE: Validates the complete MCP protocol stack works correctly with real Excel operations.
+/// PATTERN: Uses Program.ConfigureTestTransport() to inject in-memory pipes, then runs the real server.
+/// RUNTIME: ~30-60 seconds (requires Excel COM automation).
+///
+/// These tests exercise:
+/// - Full DI pipeline (exact same as production)
+/// - MCP protocol serialization/deserialization
+/// - Tool discovery and invocation via MCP protocol
+/// - Real Excel operations through COM interop
+/// - Session management across multiple tool calls
+/// - Application Insights telemetry (same configuration as production)
///
-/// PURPOSE: Fast, on-demand test to verify major functionality isn't broken.
-/// SCOPE: Exercises the 12 main MCP tools with typical LLM workflows.
-/// RUNTIME: ~30-60 seconds (fast enough for pre-commit checks).
+/// The server is a BLACK BOX - tests only interact via MCP protocol.
+/// Only the transport differs: pipes instead of stdio.
///
-/// Run this test before commits to catch breaking changes:
-/// dotnet test --filter "FullyQualifiedName~McpServerSmokeTests.SmokeTest_AllTools_LlmWorkflow"
+/// Run before commits to catch breaking changes:
+/// dotnet test --filter "FullyQualifiedName~McpServerSmokeTests"
///
[Trait("Category", "Integration")]
[Trait("Speed", "Medium")]
[Trait("Layer", "McpServer")]
[Trait("Feature", "SmokeTest")]
[Trait("RequiresExcel", "true")]
-public class McpServerSmokeTests : IDisposable
+public class McpServerSmokeTests : IAsyncLifetime, IAsyncDisposable
{
private readonly ITestOutputHelper _output;
private readonly string _tempDir;
private readonly string _testExcelFile;
private readonly string _testCsvFile;
- private readonly string _testQueryFile;
+
+ // MCP transport pipes
+ private readonly Pipe _clientToServerPipe = new();
+ private readonly Pipe _serverToClientPipe = new();
+ private readonly CancellationTokenSource _cts = new();
+ private McpClient? _client;
+ private Task? _serverTask;
public McpServerSmokeTests(ITestOutputHelper output)
{
@@ -40,14 +61,94 @@ public McpServerSmokeTests(ITestOutputHelper output)
_testExcelFile = Path.Join(_tempDir, "SmokeTest.xlsx");
_testCsvFile = Path.Join(_tempDir, "SampleData.csv");
- _testQueryFile = Path.Join(_tempDir, "TestQuery.pq");
_output.WriteLine($"Test directory: {_tempDir}");
}
- ///
- public void Dispose()
+ ///
+ /// Setup: Configure test transport and run the real MCP server.
+ /// The server is a BLACK BOX - we only configure transport, everything else is production code.
+ ///
+ public async Task InitializeAsync()
+ {
+ // Configure the server to use our test pipes instead of stdio
+ // This is the ONLY difference from production - transport layer only
+ Program.ConfigureTestTransport(_clientToServerPipe, _serverToClientPipe);
+
+ // Run the REAL server (Program.Main) - exact same code path as production
+ // The server will use our configured pipes for transport
+ _serverTask = Program.Main([]);
+
+ // Create client connected to the server via pipes
+ _client = await McpClient.CreateAsync(
+ new StreamClientTransport(
+ serverInput: _clientToServerPipe.Writer.AsStream(),
+ serverOutput: _serverToClientPipe.Reader.AsStream()),
+ clientOptions: new McpClientOptions
+ {
+ ClientInfo = new() { Name = "SmokeTestClient", Version = "1.0.0" }
+ },
+ cancellationToken: _cts.Token);
+
+ _output.WriteLine($"✓ Connected to server: {_client.ServerInfo?.Name} v{_client.ServerInfo?.Version}");
+ }
+
+ public async Task DisposeAsync()
+ {
+ await DisposeAsyncCore();
+ }
+
+ async ValueTask IAsyncDisposable.DisposeAsync()
+ {
+ await DisposeAsyncCore();
+ GC.SuppressFinalize(this);
+ }
+
+ private async Task DisposeAsyncCore()
{
+ // Flush telemetry before shutdown to ensure test telemetry is sent
+ ExcelMcpTelemetry.Flush();
+
+ // Dispose client first - this signals we're done sending requests
+ if (_client != null)
+ {
+ await _client.DisposeAsync();
+ }
+
+ // Complete the pipes to signal EOF - this triggers GRACEFUL server shutdown
+ // The MCP SDK will see EOF and stop the host naturally, allowing
+ // Application Insights and other services to flush during shutdown
+ _clientToServerPipe.Writer.Complete();
+ _serverToClientPipe.Writer.Complete();
+
+ // Wait for server to shut down gracefully (with timeout)
+ if (_serverTask != null)
+ {
+ // Give the server time to flush telemetry and clean up
+ var shutdownTimeout = Task.Delay(TimeSpan.FromSeconds(10));
+ var completed = await Task.WhenAny(_serverTask, shutdownTimeout);
+
+ if (completed == shutdownTimeout)
+ {
+ // Server didn't shut down in time - cancel as fallback
+ await _cts.CancelAsync();
+ try
+ {
+ await _serverTask;
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when we had to force cancel
+ }
+ }
+ }
+
+ // Reset test transport for next test
+ Program.ResetTestTransport();
+
+ _cts.Dispose();
+
+ // Clean up temp files
if (Directory.Exists(_tempDir))
{
try
@@ -59,308 +160,416 @@ public void Dispose()
// Ignore cleanup errors
}
}
- GC.SuppressFinalize(this);
}
///
- /// Comprehensive smoke test that exercises all 13 MCP tools in a realistic LLM workflow using the session API.
- /// This test validates the complete tool chain and demonstrates proper session usage for multiple operations.
+ /// Comprehensive smoke test that exercises all 12 MCP tools via the SDK client.
+ /// This validates the complete E2E flow: MCP protocol → DI → Tool → Core → Excel COM.
///
[Fact]
- public void SmokeTest_AllTools_LlmWorkflow()
+ public async Task SmokeTest_AllTools_E2EWorkflow()
{
- _output.WriteLine("=== MCP SERVER SMOKE TEST (SESSION API) ===");
- _output.WriteLine("Testing all 13 tools in optimized session workflow...\n");
+ _output.WriteLine("=== MCP SERVER E2E SMOKE TEST (SDK CLIENT) ===");
+ _output.WriteLine("Testing all 12 tools via MCP protocol with real Excel...\n");
// =====================================================================
- // STEP 1: FILE CREATION (before session)
+ // STEP 1: FILE CREATION
// =====================================================================
- _output.WriteLine("✓ Step 1: Creating workbook...");
+ _output.WriteLine("✓ Step 1: Creating workbook via MCP protocol...");
- // Create empty workbook
- var createResult = ExcelFileTool.ExcelFile(FileAction.CreateEmpty, _testExcelFile);
+ var createResult = await CallToolAsync("excel_file", new Dictionary
+ {
+ ["action"] = "CreateEmpty",
+ ["excelPath"] = _testExcelFile
+ });
AssertSuccess(createResult, "File creation");
Assert.True(File.Exists(_testExcelFile), "Excel file should exist");
-
- _output.WriteLine(" ✓ excel_file: CREATE passed");
+ _output.WriteLine(" ✓ excel_file: CreateEmpty passed");
// =====================================================================
- // STEP 2: OPEN SESSION (75-90% faster for multiple operations)
+ // STEP 2: OPEN SESSION
// =====================================================================
- _output.WriteLine("\n✓ Step 2: Opening session...");
+ _output.WriteLine("\n✓ Step 2: Opening session via MCP protocol...");
- var openResult = ExcelFileTool.ExcelFile(FileAction.Open, _testExcelFile);
+ var openResult = await CallToolAsync("excel_file", new Dictionary
+ {
+ ["action"] = "Open",
+ ["excelPath"] = _testExcelFile
+ });
AssertSuccess(openResult, "Open session");
- var openJson = JsonDocument.Parse(openResult);
- var sessionId = openJson.RootElement.GetProperty("sessionId").GetString();
+ var sessionId = GetJsonProperty(openResult, "sessionId");
Assert.NotNull(sessionId);
-
_output.WriteLine($" ✓ Session opened: {sessionId}");
// =====================================================================
- // STEP 3: ALL OPERATIONS IN SESSION (using sessionId)
+ // STEP 3: WORKSHEET OPERATIONS
// =====================================================================
- _output.WriteLine("\n✓ Step 3: Running all operations in active session...");
-
- // Test file (no session required for test)
- var testResult = ExcelFileTool.ExcelFile(FileAction.Test, _testExcelFile);
- AssertSuccess(testResult, "File test");
+ _output.WriteLine("\n✓ Step 3: Worksheet operations...");
- // Worksheet operations (with session)
- var listSheetsResult = ExcelWorksheetTool.ExcelWorksheet(WorksheetAction.List, sessionId);
- AssertSuccess(listSheetsResult, "List worksheets in batch");
+ var listSheetsResult = await CallToolAsync("excel_worksheet", new Dictionary
+ {
+ ["action"] = "List",
+ ["sessionId"] = sessionId
+ });
+ AssertSuccess(listSheetsResult, "List worksheets");
- var createSheetResult = ExcelWorksheetTool.ExcelWorksheet(
- WorksheetAction.Create,
- sessionId,
- sheetName: "Data");
- AssertSuccess(createSheetResult, "Create worksheet in batch");
+ var createSheetResult = await CallToolAsync("excel_worksheet", new Dictionary
+ {
+ ["action"] = "Create",
+ ["sessionId"] = sessionId,
+ ["sheetName"] = "Data"
+ });
+ AssertSuccess(createSheetResult, "Create worksheet");
+ _output.WriteLine(" ✓ excel_worksheet: List and Create passed");
- _output.WriteLine(" ✓ excel_worksheet: LIST and CREATE in batch");
+ // =====================================================================
+ // STEP 4: RANGE OPERATIONS
+ // =====================================================================
+ _output.WriteLine("\n✓ Step 4: Range operations...");
- // Range operations using session-aware tool API
var values = new List>
{
- new List