diff --git a/README.md b/README.md index d7cba1a..ae43968 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Analyze MSBuild binary logs (`.binlog`) with **GitHub Copilot Chat** and **MCP t @binlog /perf ``` -The [BinlogInsights.Mcp](https://www.nuget.org/packages/BinlogInsights.Mcp) server (28 analysis tools) is auto-installed on first use. +The [AITools.BinlogMcp](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/AITools.BinlogMcp) server (28 analysis tools) is auto-installed on first use. ## What You Get @@ -32,7 +32,7 @@ The [BinlogInsights.Mcp](https://www.nuget.org/packages/BinlogInsights.Mcp) serv | **Fix All Issues** | Copilot fixes all build errors/warnings, rebuilds, and loads before/after for comparison | | **Auto-fix Diagnostic** | Right-click any error/warning in the tree → "Auto-fix with Copilot" to fix it directly | | **Optimize Build** | Pick optimizations, Copilot applies changes, verify with A/B comparison | -| **Build Analysis Mode** | Chat mode pre-configured with BinlogInsights MCP tools — works with any agent | +| **Build Analysis Mode** | Chat mode pre-configured with AITools.BinlogMcp MCP tools — works with any agent | | **Language Model Tools** | `binlog_lm_overview`, `binlog_lm_errors`, `binlog_lm_search`, `binlog_lm_perf`, `binlog_lm_compare` — available to @workspace, agent mode, and custom chat modes | | **CI/CD Integration** | Download binlogs from Azure DevOps Pipelines and GitHub Actions — filter by branch or PR | | **Problems Panel** | Build diagnostics as native VS Code errors/warnings with per-project CodeLens and "Ask @binlog" CodeActions | @@ -52,53 +52,36 @@ The [BinlogInsights.Mcp](https://www.nuget.org/packages/BinlogInsights.Mcp) serv ## Troubleshooting: MCP Server Installation -The extension auto-installs [BinlogInsights.Mcp](https://www.nuget.org/packages/BinlogInsights.Mcp) via `dotnet tool install -g`. In corporate environments with restricted NuGet feeds, this may fail. Here are the workarounds: +The extension auto-installs [AITools.BinlogMcp](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/AITools.BinlogMcp) via `dotnet tool install -g`. In corporate environments with restricted NuGet feeds, this may fail. Here are the workarounds: -### 1. Install with explicit NuGet source +### 1. Install with explicit feed source ```bash -dotnet tool install -g BinlogInsights.Mcp --add-source https://api.nuget.org/v3/index.json +dotnet tool install -g AITools.BinlogMcp --prerelease --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json ``` -### 2. Manual download fallback - -If all `dotnet tool install` attempts fail (e.g., nuget.org is blocked): - -1. Download the `.nupkg` directly from [nuget.org](https://www.nuget.org/packages/BinlogInsights.Mcp) -2. Install from the local file: - -```bash -# Download (replace {version} with latest, e.g. 0.2.0) -Invoke-WebRequest -Uri "https://www.nuget.org/api/v2/package/BinlogInsights.Mcp/{version}" -OutFile "BinlogInsights.Mcp.nupkg" - -# Install from local file -dotnet tool install -g BinlogInsights.Mcp --add-source . -``` - -### 3. Diagnose NuGet issues +### 2. Diagnose NuGet issues ```bash dotnet nuget list source ``` Common problems: -- **nuget.org not listed or disabled** — the tool is published on nuget.org -- **Authenticated feed requires credentials** — may block fallthrough to nuget.org -- **Package source mapping** excludes nuget.org for this package +- **dotnet-eng feed not configured** — the tool is published on the dotnet-eng Azure DevOps feed +- **Authenticated feed requires credentials** — may block access to the feed +- **Package source mapping** excludes the dotnet-eng feed for this package -### 4. Verify installation +### 3. Verify installation ```bash -dotnet tool list -g | Select-String BinlogInsights -binlog-insights-mcp --help +dotnet tool list -g | Select-String AITools.BinlogMcp +binlog-mcp --help ``` -> For the full troubleshooting guide, see [BinlogInsights repo setup instructions](https://github.com/SergeyTeplyakov/BinlogInsights/blob/main/samples/repo-setup/.github/skills/build-tool-setup/SKILL.md). - ## Related Projects - [MSBuild Structured Log Viewer](https://github.com/KirillOsenkov/MSBuildStructuredLog) — WPF viewer with secrets redaction -- [BinlogInsights](https://github.com/SergeyTeplyakov/BinlogInsights) — CLI + MCP server for binlog analysis +- [AITools.BinlogMcp](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-eng/NuGet/AITools.BinlogMcp) — MCP server for binlog analysis - [MSBuild Binary Log docs](https://learn.microsoft.com/en-us/visualstudio/msbuild/obtaining-build-logs-with-msbuild#save-a-binary-log) ## License diff --git a/package.json b/package.json index db8d22c..5d5f44e 100644 --- a/package.json +++ b/package.json @@ -247,12 +247,12 @@ "binlogAnalyzer.mcpServerPath": { "type": "string", "default": "", - "description": "Custom path to the MCP server dotnet tool (BinlogInsights.Mcp or baronfel.binlog.mcp). Leave empty to use the globally installed tool." + "description": "Custom path to the MCP server dotnet tool (AITools.BinlogMcp). Leave empty to use the globally installed tool." }, "binlogAnalyzer.mcpServerArgs": { "type": "string", "default": "--binlog ${binlog}", - "markdownDescription": "Argument template for the MCP server. Use `${binlog}` as a placeholder for each binlog path. Repeated for each loaded binlog. Examples:\n- BinlogInsights: `--binlog ${binlog}` (default)\n- baronfel: `--binlog ${binlog}`\n- Custom: `--file ${binlog}`" + "markdownDescription": "Argument template for the MCP server. Use `${binlog}` as a placeholder for each binlog path. Repeated for each loaded binlog. Examples:\n- AITools.BinlogMcp: `--binlog ${binlog}` (default)\n- Custom: `--file ${binlog}`" }, "binlogAnalyzer.autoLoad": { "type": "boolean", @@ -520,14 +520,14 @@ { "slug": "build-analysis", "name": "Build Analysis", - "description": "Pre-configured for MSBuild binlog investigation with BinlogInsights MCP tools.", + "description": "Pre-configured for MSBuild binlog investigation with AITools.BinlogMcp MCP tools.", "toolsReferences": [ { "type": "tool", "id": "binlog_insights_mcp" } ], - "systemPrompt": "You are an MSBuild build analysis assistant with BinlogInsights MCP tools available. Begin investigations with binlog_lm_overview (or binlog_overview) unless the question is narrowly scoped, then drill in with the most specific tool. For performance: rank with binlog_expensive_targets / binlog_expensive_tasks / binlog_expensive_projects / binlog_expensive_analyzers. For errors: binlog_lm_errors (or binlog_errors) then binlog_lm_search (or binlog_search). For deeper context the @binlog chat participant exposes the same tools plus prepared playbooks (try /perf, /incremental, /summary, /errors, /buildcheck). All tools take a binlog_file path parameter — ask the user if you don't know it. Reference real file paths, line numbers and error codes from tool output; don't invent numbers." + "systemPrompt": "You are an MSBuild build analysis assistant with AITools.BinlogMcp MCP tools available. Begin investigations with binlog_lm_overview (or binlog_overview) unless the question is narrowly scoped, then drill in with the most specific tool. For performance: rank with binlog_expensive_targets / binlog_expensive_tasks / binlog_expensive_projects / binlog_expensive_analyzers. For errors: binlog_lm_errors (or binlog_errors) then binlog_lm_search (or binlog_search). For deeper context the @binlog chat participant exposes the same tools plus prepared playbooks (try /perf, /incremental, /summary, /errors, /buildcheck). All tools take a binlog_file path parameter — ask the user if you don't know it. Reference real file paths, line numbers and error codes from tool output; don't invent numbers." } ], "menus": { diff --git a/resources/playbooks/core.md b/resources/playbooks/core.md index 86c8492..f9eaf1d 100644 --- a/resources/playbooks/core.md +++ b/resources/playbooks/core.md @@ -1,4 +1,4 @@ -You are an MSBuild build analysis assistant embedded in VS Code. The user has loaded one or more `.binlog` files (binary build logs) and you have BinlogInsights MCP tools available to inspect them. +You are an MSBuild build analysis assistant embedded in VS Code. The user has loaded one or more `.binlog` files (binary build logs) and you have AITools.BinlogMcp MCP tools available to inspect them. Workflow contract: - Act on the user's request immediately. Do not ask the user for permission, do not ask "would you like me to…", do not echo the request back. Just call the right tool. diff --git a/src/binlogDocumentProvider.ts b/src/binlogDocumentProvider.ts index e70a684..6d83061 100644 --- a/src/binlogDocumentProvider.ts +++ b/src/binlogDocumentProvider.ts @@ -119,7 +119,21 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide // Build overview try { const overviewResult = await this.call('binlog_overview', {}, binlogPath); - const ov = JSON.parse(overviewResult.text); + // Handle both JSON (BinlogInsights) and text (AITools.BinlogMcp) formats + let ov: any; + try { + ov = JSON.parse(overviewResult.text); + } catch { + // AITools.BinlogMcp returns human-readable text — parse it + const text = overviewResult.text; + ov = { + succeeded: /SUCCEEDED/i.test(text), + msBuildVersion: text.match(/MSBuild:\s*(.+)/)?.[1]?.trim() || '', + errorCount: parseInt(text.match(/Errors:\s*(\d+)/)?.[1] || '0'), + warningCount: parseInt(text.match(/Warnings:\s*(\d+)/)?.[1] || '0'), + duration: text.match(/Duration:\s*(.+)/)?.[1]?.trim() || '', + }; + } const status = ov.succeeded ? '✅ BUILD SUCCEEDED' : '❌ BUILD FAILED'; const dur = ov.duration || ''; // Parse "HH:MM:SS.xxx" duration to a readable format @@ -152,8 +166,8 @@ export class BinlogDocumentProvider implements vscode.TextDocumentContentProvide // Handle both formats: array (BinlogInsights) and object (baronfel) let projectFiles: string[] = []; if (Array.isArray(projData)) { - // BinlogInsights: [{ fullPath, isLegacy }, ...] - projectFiles = projData.map((p: any) => p.fullPath || '').filter(Boolean); + // Array format: [{ fullPath/projectFile, ... }, ...] + projectFiles = projData.map((p: any) => p.fullPath || p.projectFile || '').filter(Boolean); } else { // baronfel: { "id": { projectFile, entryTargets }, ... } projectFiles = Object.values(projData as Record) diff --git a/src/binlogTreeView.ts b/src/binlogTreeView.ts index 9e67e93..55c598c 100644 --- a/src/binlogTreeView.ts +++ b/src/binlogTreeView.ts @@ -74,6 +74,8 @@ interface TreeNodeData { itemType?: string; /** For evaluation nodes: evaluation id from MCP */ evaluationId?: number; + /** For task nodes: task id from MCP */ + taskId?: number; } export class BinlogTreeDataProvider implements vscode.TreeDataProvider { @@ -878,10 +880,15 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider 0 || + (checkText && !checkText.startsWith('No results') && checkText.includes('Total analyzer execution')); - if (checkEntries.length === 0) { + if (!hasResults) { this.analyzersCache = []; return [this.makeInfoItem('No analyzer data found', 'info')]; } @@ -915,43 +922,19 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider 0.001 && name.length > 5 && !name.startsWith('Total')) { - const durationMs = Math.round(seconds * 1000); - const existing = analyzerMap.get(name); - if (existing) { - existing.durationMs += durationMs; - existing.count++; - } else { - analyzerMap.set(name, { durationMs, count: 1 }); - } - } - continue; - } - // Match assembly-level summary: "363 ms AssemblyFullName, Version=... : AnalyzerName = 341 ms" - const asmMatch = msg.match(/^(\d+)\s*ms\s{2,}(.+)/); - if (asmMatch && entry.nodeType === 'Item') { - const durationMs = parseInt(asmMatch[1]); - let name = asmMatch[2].trim(); - // Trim version info: "Name, Version=... : SubName = Nms" → just "Name" - const colonIdx = name.indexOf(':'); - if (colonIdx > 0) { name = name.substring(0, colonIdx).trim(); } - const commaIdx = name.indexOf(','); - if (commaIdx > 0) { name = name.substring(0, commaIdx).trim(); } - if (durationMs > 0 && name.length > 3) { - const existing = analyzerMap.get(name); - if (existing) { - existing.durationMs += durationMs; - existing.count++; - } else { - analyzerMap.set(name, { durationMs, count: 1 }); - } + this.extractAnalyzerTiming(msg, entry.nodeType, analyzerMap); + } + + // Process text lines (new AITools.BinlogMcp format) + if (entries.length === 0 && result.text && !result.text.startsWith('No results')) { + for (const line of result.text.split('\n')) { + const msgMatch = line.match(/^\s*\[(\w+)\]\s*(.+)/); + if (msgMatch) { + this.extractAnalyzerTiming(msgMatch[2].trim(), msgMatch[1], analyzerMap); } } } @@ -989,6 +972,46 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider): void { + // Match individual analyzer timing: "0.176 71 FullAnalyzerName (CA1234)" + const timingMatch = msg.match(/^(\d+\.\d+)\s+\d+\s{2,}(.+)/); + if (timingMatch) { + const seconds = parseFloat(timingMatch[1]); + const name = timingMatch[2].trim(); + if (seconds > 0.001 && name.length > 5 && !name.startsWith('Total')) { + const durationMs = Math.round(seconds * 1000); + const existing = analyzerMap.get(name); + if (existing) { + existing.durationMs += durationMs; + existing.count++; + } else { + analyzerMap.set(name, { durationMs, count: 1 }); + } + } + return; + } + // Match assembly-level summary: "363 ms AssemblyFullName, Version=... : AnalyzerName = 341 ms" + const asmMatch = msg.match(/^(\d+)\s*ms\s{2,}(.+)/); + if (asmMatch && (nodeType === 'Item' || nodeType === undefined)) { + const durationMs = parseInt(asmMatch[1]); + let name = asmMatch[2].trim(); + const colonIdx = name.indexOf(':'); + if (colonIdx > 0) { name = name.substring(0, colonIdx).trim(); } + const commaIdx = name.indexOf(','); + if (commaIdx > 0) { name = name.substring(0, commaIdx).trim(); } + if (durationMs > 0 && name.length > 3) { + const existing = analyzerMap.get(name); + if (existing) { + existing.durationMs += durationMs; + existing.count++; + } else { + analyzerMap.set(name, { durationMs, count: 1 }); + } + } + } + } + private parsePerfItems(text: string, icon: string): TreeNodeData[] { const data = this.tryParseJson(text); const items: TreeNodeData[] = []; @@ -1118,7 +1141,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider = { - target_name: targetName, + target: targetName, project: projectName, }; const result = await this.mcpCall('binlog_tasks_in_target', args); @@ -1140,6 +1163,7 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider = { - task_name: taskName, - project: projectName, - target_name: element.targetName || '', - }; + const args: Record = {}; + if (element.taskId !== undefined) { + // New MCP: use task_id directly + args.task_id = element.taskId; + } else { + // Legacy fallback: look up by name + const projectName = element.projectFile + ? this.extractFileName(element.projectFile).replace(/\.[^.]+$/, '') + : ''; + args.task_name = taskName; + args.project = projectName; + args.target_name = element.targetName || ''; + } const result = await this.mcpCall('binlog_task_details', args); const items: BinlogTreeItem[] = []; @@ -1438,8 +1467,25 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider { const file = e.projectFile || e.ProjectFile || ''; @@ -1465,6 +1511,10 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider 1) { - args.binlog_file = this.binlogPaths[0]; + if (!args.binlog_file && this.binlogPaths.length >= 1) { + args.binlog_file = this.activeBinlogPath || this.binlogPaths[0]; } this.loadingSet.add(parentKind); @@ -1619,8 +1667,8 @@ export class BinlogTreeDataProvider implements vscode.TreeDataProvider = {}): Promise<{ text: string }> { if (!this.mcpClient) { throw new Error('MCP server not connected'); } - if (!args.binlog_file && this.binlogPaths.length > 1) { - args.binlog_file = this.binlogPaths[0]; + if (!args.binlog_file && this.binlogPaths.length >= 1) { + args.binlog_file = this.activeBinlogPath || this.binlogPaths[0]; } return this.mcpClient.callTool(tool, args); } @@ -1838,6 +1886,8 @@ export class BinlogTreeItem extends vscode.TreeItem { itemType?: string; /** For evaluation nodes: evaluation id from MCP */ evaluationId?: number; + /** For task nodes: task id from MCP */ + taskId?: number; constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState) { super(label, collapsibleState); } diff --git a/src/chatParticipant.ts b/src/chatParticipant.ts index 4a9b853..4abecb5 100644 --- a/src/chatParticipant.ts +++ b/src/chatParticipant.ts @@ -129,7 +129,7 @@ export class BinlogChatParticipant { if (tools.length === 0 && !isBuildCheck) { stream.markdown( '⚠️ No binlog MCP tools found. The MCP server may not be running.\n\n' + - '1. Check `dotnet tool list -g` for `BinlogInsights.Mcp`\n' + + '1. Check `dotnet tool list -g` for `AITools.BinlogMcp`\n' + '2. Restart VS Code to reload MCP servers\n', ); return; diff --git a/src/extension.ts b/src/extension.ts index fe02fc2..18cad8e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,7 +29,8 @@ let statusBarItem: vscode.StatusBarItem | undefined; let extensionContext: vscode.ExtensionContext | undefined; let openedViaUri = false; let optimizeInProgress = false; -let cachedInsightsExePath: string | null | undefined; // undefined = not searched yet +let cachedMcpExePath: string | null | undefined; // undefined = not searched yet +const DOTNET_ENG_FEED = 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json'; let codeLensRegistered = false; function escapeHtml(s: string): string { @@ -1150,8 +1151,7 @@ export async function activate(context: vscode.ExtensionContext) { let results: any[] = []; try { - const data = JSON.parse(result.text); - results = Array.isArray(data) ? data : []; + results = parseSearchResults(result.text); } catch { results = []; } @@ -1204,8 +1204,7 @@ export async function activate(context: vscode.ExtensionContext) { }); let page: any[] = []; try { - const data = JSON.parse(result.text); - page = Array.isArray(data) ? data : []; + page = parseSearchResults(result.text); } catch { break; } allResults.push(...page); if (page.length < pageSize) { break; } @@ -2029,7 +2028,7 @@ async function startMcpClientForTree(binlogPaths: string[]) { const config = vscode.workspace.getConfiguration('binlogAnalyzer'); const customPath = config.get('mcpServerPath', ''); - let toolExe = customPath || findBinlogInsightsTool(); + let toolExe = customPath || findMcpTool(); if (!toolExe) { // Don't block tree loading with install — configureMcpServer handles install return; @@ -2100,12 +2099,12 @@ async function configureMcpServer(binlogPaths: string[], config: vscode.Workspac ...(workspaceCwd && { cwd: workspaceCwd }), }; } else { - let insightsExe = findBinlogInsightsTool(); + let insightsExe = findMcpTool(); if (!insightsExe) { - insightsExe = await installBinlogInsightsTool(); + insightsExe = await installMcpTool(); // After install, start the tree client (it skipped earlier because tool wasn't found) if (insightsExe) { - cachedInsightsExePath = insightsExe; + cachedMcpExePath = insightsExe; startMcpClientForTree(binlogPaths).then(() => { treeDataProvider?.setLoading(false); updateStatusBar(); @@ -2113,7 +2112,7 @@ async function configureMcpServer(binlogPaths: string[], config: vscode.Workspac treeDataProvider?.setLoading(false); updateStatusBar(); surfaceMcpError('startMcpClientForTree:postInstall', err, { - userMessage: 'BinlogInsights installed but the MCP server failed to start. Reload the window.', + userMessage: 'AITools.BinlogMcp installed but the MCP server failed to start. Reload the window.', }); }); } @@ -2131,15 +2130,15 @@ async function configureMcpServer(binlogPaths: string[], config: vscode.Workspac insightsConfig = { type: 'stdio', command: 'dotnet', - args: ['tool', 'run', 'binlog-insights-mcp', '--', ...binlogArgs], + args: ['tool', 'run', 'binlog-mcp', '--', ...binlogArgs], ...(workspaceCwd && { cwd: workspaceCwd }), }; vscode.window.showWarningMessage( - 'Could not find or install BinlogInsights.Mcp. Install it manually: `dotnet tool install -g BinlogInsights.Mcp`', + 'Could not find or install AITools.BinlogMcp. Install it manually: `dotnet tool install -g AITools.BinlogMcp --prerelease --add-source ' + DOTNET_ENG_FEED + '`', 'Copy Command' ).then(sel => { if (sel === 'Copy Command') { - vscode.env.clipboard.writeText('dotnet tool install -g BinlogInsights.Mcp'); + vscode.env.clipboard.writeText('dotnet tool install -g AITools.BinlogMcp --prerelease --add-source ' + DOTNET_ENG_FEED); } }); } @@ -2201,14 +2200,14 @@ async function writeUserMcpJson(serverConfig: Record) { } catch { /* non-fatal */ } } -const NUGET_PACKAGE_ID = 'BinlogInsights.Mcp'; +const NUGET_PACKAGE_ID = 'AITools.BinlogMcp'; -/** Checks whether a server config entry refers to the BinlogInsights MCP server. */ -function serverMatchesBinlogInsights(s: any): boolean { +/** Checks whether a server config entry refers to the binlog MCP server. */ +function serverMatchesBinlogMcp(s: any): boolean { const cmd = typeof s?.command === 'string' ? s.command : ''; const args: string[] = Array.isArray(s?.args) ? s.args : []; const combined = [cmd, ...args].join(' ').toLowerCase(); - return combined.includes('binlog-insights-mcp') || combined.includes('binloginsights.mcp'); + return combined.includes('binlog-mcp') || combined.includes('binloginsights.mcp') || combined.includes('aitools.binlogmcp'); } /** Returns paths to mcp.json files that define the binlog-insights server. */ @@ -2223,7 +2222,7 @@ function getMcpConfigPaths(): string[] { try { const content = JSON.parse(fs.readFileSync(wsMcp, 'utf8')); const servers = content.servers || {}; - if (Object.values(servers).some((s: any) => serverMatchesBinlogInsights(s))) { + if (Object.values(servers).some((s: any) => serverMatchesBinlogMcp(s))) { paths.push(wsMcp); } } catch { /* ignore parse errors */ } @@ -2250,7 +2249,7 @@ function getMcpConfigPaths(): string[] { } async function fetchAboutInfo(mode: 'interactive' | 'auto' | 'silent') { - const toolPath = findBinlogInsightsTool(); + const toolPath = findMcpTool(); const version = toolPath ? await getInstalledMcpVersion(toolPath) : null; const latestVersion = await getLatestNuGetVersion(); @@ -2267,23 +2266,23 @@ async function fetchAboutInfo(mode: 'interactive' | 'auto' | 'silent') { if ((mode === 'interactive' || mode === 'auto') && updateAvailable) { const choice = await vscode.window.showInformationMessage( - `BinlogInsights.Mcp update available: v${version} → v${latestVersion}`, + `AITools.BinlogMcp update available: v${version} → v${latestVersion}`, 'Update Now' ); if (choice === 'Update Now') { await updateMcpServer(); } } else if (mode === 'interactive' && !updateAvailable && version) { - vscode.window.showInformationMessage(`BinlogInsights.Mcp v${version} is up to date.`); + vscode.window.showInformationMessage(`AITools.BinlogMcp v${version} is up to date.`); } } async function getInstalledMcpVersion(toolPath: string): Promise { // Primary: read version from the .store directory (works for all versions, even old ones without --version) try { - const storeDir = path.join(os.homedir(), '.dotnet', 'tools', '.store', 'binloginsights.mcp'); + const storeDir = path.join(os.homedir(), '.dotnet', 'tools', '.store', 'aitools.binlogmcp'); if (fs.existsSync(storeDir)) { - const versions = fs.readdirSync(storeDir).filter(d => /^\d+\.\d+\.\d+$/.test(d)); + const versions = fs.readdirSync(storeDir).filter(d => /^\d+\.\d+\.\d+/.test(d)); if (versions.length > 0) { // Sort and pick the highest (there should only be one for a global tool) versions.sort(compareVersions); @@ -2309,8 +2308,9 @@ async function getLatestNuGetVersion(): Promise { const cp = require('child_process'); return new Promise((resolve) => { - // Use dotnet CLI which respects all configured NuGet sources (including local feeds) - cp.execFile('dotnet', ['package', 'search', NUGET_PACKAGE_ID, '--exact-match', '--format', 'json'], + // Search the dotnet-eng feed for the package (includes prereleases) + cp.execFile('dotnet', ['package', 'search', NUGET_PACKAGE_ID, '--exact-match', '--format', 'json', '--prerelease', + '--source', DOTNET_ENG_FEED], { timeout: 30000, encoding: 'utf8' }, (error: any, stdout: string) => { if (error) { @@ -2329,11 +2329,9 @@ async function getLatestNuGetVersion(): Promise { } } } - // Filter out prereleases and find the highest version - const stable = allVersions.filter(v => !v.includes('-')); - if (stable.length === 0) { resolve(null); return; } - stable.sort(compareVersions); - resolve(stable[stable.length - 1]); + if (allVersions.length === 0) { resolve(null); return; } + allVersions.sort(compareVersions); + resolve(allVersions[allVersions.length - 1]); } catch { resolve(null); } @@ -2359,16 +2357,16 @@ async function updateMcpServer() { // No binlog loaded — no MCP server running from our extension, try direct update const cp = require('child_process'); const result = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'Updating BinlogInsights MCP server...' }, + { location: vscode.ProgressLocation.Notification, title: 'Updating AITools.BinlogMcp MCP server...' }, () => new Promise<{ success: boolean; output: string }>((resolve) => { - cp.execFile('dotnet', ['tool', 'update', '-g', 'BinlogInsights.Mcp'], { timeout: 60000 }, (err: Error | null, stdout: string, stderr: string) => { + cp.execFile('dotnet', ['tool', 'update', '-g', 'AITools.BinlogMcp', '--prerelease', '--add-source', DOTNET_ENG_FEED], { timeout: 60000 }, (err: Error | null, stdout: string, stderr: string) => { resolve({ success: !err, output: (stderr || stdout || '').toString() }); }); }) ); - cachedInsightsExePath = undefined; + cachedMcpExePath = undefined; if (result.success) { - vscode.window.showInformationMessage('BinlogInsights MCP server updated successfully.'); + vscode.window.showInformationMessage('AITools.BinlogMcp MCP server updated successfully.'); await fetchAboutInfo('silent'); } else { vscode.window.showErrorMessage(`Failed to update: ${result.output.substring(0, 200)}`); @@ -2399,9 +2397,9 @@ async function applyPendingToolUpdate(): Promise { const cp = require('child_process'); const result = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'Updating BinlogInsights MCP server...' }, + { location: vscode.ProgressLocation.Notification, title: 'Updating AITools.BinlogMcp MCP server...' }, () => new Promise<{ success: boolean; output: string }>((resolve) => { - cp.execFile('dotnet', ['tool', 'update', '-g', 'BinlogInsights.Mcp'], { timeout: 60000 }, (err: Error | null, stdout: string, stderr: string) => { + cp.execFile('dotnet', ['tool', 'update', '-g', 'AITools.BinlogMcp', '--prerelease', '--add-source', DOTNET_ENG_FEED], { timeout: 60000 }, (err: Error | null, stdout: string, stderr: string) => { resolve({ success: !err, output: (stderr || stdout || '').toString() }); }); }) @@ -2409,25 +2407,25 @@ async function applyPendingToolUpdate(): Promise { // Don't clear binlog.updatingMcp here — the auto-restore will clear it after loading binlogs, // or we clear it below if there are no binlogs to restore. - cachedInsightsExePath = undefined; + cachedMcpExePath = undefined; if (result.success) { - vscode.window.showInformationMessage('BinlogInsights MCP server updated successfully.'); + vscode.window.showInformationMessage('AITools.BinlogMcp MCP server updated successfully.'); } else { - vscode.window.showErrorMessage(`Failed to update BinlogInsights.Mcp: ${result.output.substring(0, 200)}`); + vscode.window.showErrorMessage(`Failed to update AITools.BinlogMcp: ${result.output.substring(0, 200)}`); } } -function findBinlogInsightsTool(): string | null { - if (cachedInsightsExePath !== undefined) { return cachedInsightsExePath; } +function findMcpTool(): string | null { + if (cachedMcpExePath !== undefined) { return cachedMcpExePath; } const homeDir = os.homedir(); const isWindows = process.platform === 'win32'; - const exeName = isWindows ? 'binlog-insights-mcp.exe' : 'binlog-insights-mcp'; + const exeName = isWindows ? 'binlog-mcp.exe' : 'binlog-mcp'; // Global dotnet tools are installed in ~/.dotnet/tools/ const globalToolPath = path.join(homeDir, '.dotnet', 'tools', exeName); if (fs.existsSync(globalToolPath)) { - cachedInsightsExePath = globalToolPath; + cachedMcpExePath = globalToolPath; return globalToolPath; } @@ -2437,7 +2435,7 @@ function findBinlogInsightsTool(): string | null { const candidate = path.join(dir, exeName); try { if (fs.existsSync(candidate)) { - cachedInsightsExePath = candidate; + cachedMcpExePath = candidate; return candidate; } } catch { @@ -2445,28 +2443,28 @@ function findBinlogInsightsTool(): string | null { } } - cachedInsightsExePath = null; + cachedMcpExePath = null; return null; } -async function installBinlogInsightsTool(): Promise { - cachedInsightsExePath = undefined; // Reset cache so findBinlogInsightsTool re-scans after install +async function installMcpTool(): Promise { + cachedMcpExePath = undefined; // Reset cache so findMcpTool re-scans after install const cp = require('child_process'); const result = await vscode.window.withProgress( - { location: vscode.ProgressLocation.Notification, title: 'Installing BinlogInsights MCP server (dotnet tool)...' }, + { location: vscode.ProgressLocation.Notification, title: 'Installing AITools.BinlogMcp MCP server (dotnet tool)...' }, () => new Promise((resolve) => { - cp.execFile('dotnet', ['tool', 'install', '-g', 'BinlogInsights.Mcp'], { timeout: 60000 }, (err: Error | null) => { + cp.execFile('dotnet', ['tool', 'install', '-g', 'AITools.BinlogMcp', '--prerelease', '--add-source', DOTNET_ENG_FEED], { timeout: 60000 }, (err: Error | null) => { if (err) { - cp.execFile('dotnet', ['tool', 'update', '-g', 'BinlogInsights.Mcp'], { timeout: 60000 }, () => { - const exe = findBinlogInsightsTool(); + cp.execFile('dotnet', ['tool', 'update', '-g', 'AITools.BinlogMcp', '--prerelease', '--add-source', DOTNET_ENG_FEED], { timeout: 60000 }, () => { + const exe = findMcpTool(); telemetry.trackToolInstall(!!exe); resolve(exe); }); } else { - const exe = findBinlogInsightsTool(); + const exe = findMcpTool(); telemetry.trackToolInstall(!!exe); if (exe) { - vscode.window.showInformationMessage('✅ BinlogInsights MCP server installed successfully.'); + vscode.window.showInformationMessage('✅ AITools.BinlogMcp MCP server installed successfully.'); } resolve(exe); } @@ -2481,6 +2479,45 @@ function getFileName(filePath: string): string { return filePath.split(/[/\\]/).pop() || filePath; } +/** + * Parse binlog_search results from either JSON array (BinlogInsights) or + * human-readable text (AITools.BinlogMcp) into a uniform array of objects. + */ +function parseSearchResults(text: string): any[] { + // Try JSON first + try { + const data = JSON.parse(text); + if (Array.isArray(data)) { return data; } + } catch { /* not JSON */ } + + // Parse AITools.BinlogMcp text format: + // Search 'query': N results + // [NodeType] Message text... + // Project: file.csproj Target: Name Task: Name + if (text.startsWith('No results')) { return []; } + const results: any[] = []; + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const entryMatch = line.match(/^\s*\[(\w+)\]\s*(.+)/); + if (!entryMatch) { continue; } + const entry: any = { nodeType: entryMatch[1], message: entryMatch[2].trim() }; + // Check next line for Project/Target/Task metadata + if (i + 1 < lines.length) { + const meta = lines[i + 1]; + const projMatch = meta.match(/Project:\s*(\S+)/); + if (projMatch) { entry.projectFile = projMatch[1]; } + const targetMatch = meta.match(/Target:\s*(\S+)/); + if (targetMatch) { entry.targetName = targetMatch[1]; } + const taskMatch = meta.match(/Task:\s*(\S+)/); + if (taskMatch) { entry.taskName = taskMatch[1]; } + if (projMatch || targetMatch || taskMatch) { i++; } + } + results.push(entry); + } + return results; +} + export function deactivate() { diagnosticsProvider?.dispose(); mcpClient?.dispose(); diff --git a/src/languageModelTools.ts b/src/languageModelTools.ts index 1b1154b..271a6e6 100644 --- a/src/languageModelTools.ts +++ b/src/languageModelTools.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { McpClient } from './mcpClient'; /** - * Bridges BinlogInsights MCP tools into VS Code's `vscode.lm.tools` registry. + * Bridges AITools.BinlogMcp MCP tools into VS Code's `vscode.lm.tools` registry. * * Why this exists * --------------- @@ -143,9 +143,30 @@ class BinlogLmTool implements vscode.LanguageModelTool { try { if (this.mcpToolName) { + let resultText: string; const result = await client.callTool(this.mcpToolName, args); + resultText = result.text || '(empty result)'; + + // Enrich overview with deduplicated project count from binlog_projects + if (this.toolName === 'binlog_lm_overview') { + try { + const projResult = await client.callTool('binlog_projects', args); + const projData = JSON.parse(projResult.text); + if (Array.isArray(projData)) { + const seen = new Set(); + for (const p of projData) { + const fp = (p.fullPath || p.projectFile || '').split(/[/\\]/).pop()?.toLowerCase() || ''; + if (fp) { seen.add(fp); } + } + if (seen.size > 0) { + resultText += `\nUnique projects: ${seen.size} (total evaluations may be higher due to restore/multi-targeting)`; + } + } + } catch { /* non-fatal */ } + } + return new vscode.LanguageModelToolResult([ - new vscode.LanguageModelTextPart(result.text || '(empty result)'), + new vscode.LanguageModelTextPart(resultText), ]); } diff --git a/src/mcpClient.ts b/src/mcpClient.ts index f56747e..a73fb68 100644 --- a/src/mcpClient.ts +++ b/src/mcpClient.ts @@ -17,7 +17,7 @@ function log(msg: string) { /** * Minimal MCP (Model Context Protocol) clientthat communicates with - * BinlogInsights.Mcp over stdio using JSON-RPC 2.0 with newline-delimited JSON. + * AITools.BinlogMcp over stdio using JSON-RPC 2.0 with newline-delimited JSON. * * Emits: * - 'unexpected-exit' when the server process exits unexpectedly (not via dispose())