From 51247772e64d0a944b6cd7350fab7e52a5393da9 Mon Sep 17 00:00:00 2001 From: KyleTryon Date: Mon, 29 Dec 2025 13:22:50 -0500 Subject: [PATCH] refactor(mcp): Enhance MCP configuration functions to return results - Updated `addCursorMcpConfig`, `addVsCodeMcpConfig`, `addClaudeCodeMcpConfig`, and `addOpenCodeMcpConfig` functions to return an object indicating the filename and action (created/updated) instead of logging directly. - Consolidated success messages in `offerProjectScopedMcpConfig` to provide a summary of created and updated files. - Improved logging for better clarity and consistency across MCP configuration processes. --- src/utils/clack/mcp-config.ts | 120 ++++++++++++++-------------- test/utils/clack/mcp-config.test.ts | 33 +++++--- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/src/utils/clack/mcp-config.ts b/src/utils/clack/mcp-config.ts index 99c0655d3..6fc9aaa6b 100644 --- a/src/utils/clack/mcp-config.ts +++ b/src/utils/clack/mcp-config.ts @@ -143,21 +143,24 @@ function getGenericMcpJsonSnippet( return JSON.stringify(obj, null, 2); } +type McpConfigResult = { + filename: string; + action: 'created' | 'updated'; +}; + async function addCursorMcpConfig( orgSlug?: string, projectSlug?: string, -): Promise { - const file = path.join(process.cwd(), '.cursor', 'mcp.json'); +): Promise { + const filename = path.join('.cursor', 'mcp.json'); + const file = path.join(process.cwd(), filename); const existing = await readJsonIfExists(file); if (!existing) { await writeJson( file, JSON.parse(getCursorMcpJsonSnippet(orgSlug, projectSlug)), ); - clack.log.success( - chalk.cyan(path.join('.cursor', 'mcp.json')) + ' created.', - ); - return; + return { filename, action: 'created' }; } try { const updated = { ...existing } as CursorMcpConfig; @@ -166,7 +169,7 @@ async function addCursorMcpConfig( url: getMcpUrl(orgSlug, projectSlug), }; await writeJson(file, updated); - clack.log.success('Updated .cursor/mcp.json'); + return { filename, action: 'updated' }; } catch { throw new Error('Failed to update .cursor/mcp.json'); } @@ -175,18 +178,16 @@ async function addCursorMcpConfig( async function addVsCodeMcpConfig( orgSlug?: string, projectSlug?: string, -): Promise { - const file = path.join(process.cwd(), '.vscode', 'mcp.json'); +): Promise { + const filename = path.join('.vscode', 'mcp.json'); + const file = path.join(process.cwd(), filename); const existing = await readJsonIfExists(file); if (!existing) { await writeJson( file, JSON.parse(getVsCodeMcpJsonSnippet(orgSlug, projectSlug)), ); - clack.log.success( - chalk.cyan(path.join('.vscode', 'mcp.json')) + ' created.', - ); - return; + return { filename, action: 'created' }; } try { const updated = { ...existing } as VsCodeMcpConfig; @@ -196,7 +197,7 @@ async function addVsCodeMcpConfig( type: 'http', }; await writeJson(file, updated); - clack.log.success('Updated .vscode/mcp.json'); + return { filename, action: 'updated' }; } catch { throw new Error('Failed to update .vscode/mcp.json'); } @@ -205,16 +206,16 @@ async function addVsCodeMcpConfig( async function addClaudeCodeMcpConfig( orgSlug?: string, projectSlug?: string, -): Promise { - const file = path.join(process.cwd(), '.mcp.json'); +): Promise { + const filename = '.mcp.json'; + const file = path.join(process.cwd(), filename); const existing = await readJsonIfExists(file); if (!existing) { await writeJson( file, JSON.parse(getClaudeCodeMcpJsonSnippet(orgSlug, projectSlug)), ); - clack.log.success(chalk.cyan('.mcp.json') + ' created.'); - return; + return { filename, action: 'created' }; } try { const updated = { ...existing } as ClaudeCodeMcpConfig; @@ -223,7 +224,7 @@ async function addClaudeCodeMcpConfig( url: getMcpUrl(orgSlug, projectSlug), }; await writeJson(file, updated); - clack.log.success('Updated .mcp.json'); + return { filename, action: 'updated' }; } catch { throw new Error('Failed to update .mcp.json'); } @@ -232,16 +233,16 @@ async function addClaudeCodeMcpConfig( async function addOpenCodeMcpConfig( orgSlug?: string, projectSlug?: string, -): Promise { - const file = path.join(process.cwd(), 'opencode.json'); +): Promise { + const filename = 'opencode.json'; + const file = path.join(process.cwd(), filename); const existing = await readJsonIfExists(file); if (!existing) { await writeJson( file, JSON.parse(getOpenCodeMcpJsonSnippet(orgSlug, projectSlug)), ); - clack.log.success(chalk.cyan('opencode.json') + ' created.'); - return; + return { filename, action: 'created' }; } try { const updated = { ...existing } as OpenCodeMcpConfig; @@ -253,7 +254,7 @@ async function addOpenCodeMcpConfig( oauth: {}, }; await writeJson(file, updated); - clack.log.success('Updated opencode.json'); + return { filename, action: 'updated' }; } catch { throw new Error('Failed to update opencode.json'); } @@ -534,6 +535,10 @@ export async function offerProjectScopedMcpConfig( // Track number of editors selected Sentry.setTag('mcp-editors-count', editors.length); + // Collect results for auto-configured editors to show consolidated output + const configResults: McpConfigResult[] = []; + let hasOpenCode = false; + // Configure each selected editor for (const editor of editors) { // Track which editor is being configured @@ -542,48 +547,19 @@ export async function offerProjectScopedMcpConfig( try { switch (editor) { case 'cursor': - await addCursorMcpConfig(orgSlug, projectSlug); - clack.log.success( - 'Added project-scoped Sentry MCP configuration for Cursor.', - ); - clack.log.info( - chalk.dim( - 'Note: You may need to reload your editor for MCP changes to take effect.', - ), - ); + configResults.push(await addCursorMcpConfig(orgSlug, projectSlug)); break; case 'vscode': - await addVsCodeMcpConfig(orgSlug, projectSlug); - clack.log.success( - 'Added project-scoped Sentry MCP configuration for VS Code.', - ); - clack.log.info( - chalk.dim( - 'Note: You may need to reload your editor for MCP changes to take effect.', - ), - ); + configResults.push(await addVsCodeMcpConfig(orgSlug, projectSlug)); break; case 'claudeCode': - await addClaudeCodeMcpConfig(orgSlug, projectSlug); - clack.log.success( - 'Added project-scoped Sentry MCP configuration for Claude Code.', - ); - clack.log.info( - chalk.dim( - 'Note: You may need to reload your editor for MCP changes to take effect.', - ), + configResults.push( + await addClaudeCodeMcpConfig(orgSlug, projectSlug), ); break; case 'openCode': - await addOpenCodeMcpConfig(orgSlug, projectSlug); - clack.log.success( - 'Added project-scoped Sentry MCP configuration for OpenCode.', - ); - clack.log.info( - chalk.dim( - 'Note: You may need to restart OpenCode for MCP changes to take effect.', - ), - ); + configResults.push(await addOpenCodeMcpConfig(orgSlug, projectSlug)); + hasOpenCode = true; break; case 'jetbrains': await showJetBrainsMcpConfig(orgSlug, projectSlug); @@ -632,4 +608,30 @@ export async function offerProjectScopedMcpConfig( } } } + + // Show consolidated output for auto-configured editors + if (configResults.length > 0) { + const created = configResults.filter((r) => r.action === 'created'); + const updated = configResults.filter((r) => r.action === 'updated'); + + const parts: string[] = []; + if (created.length > 0) { + const files = created.map((r) => chalk.cyan(r.filename)).join(' and '); + parts.push(`${files} created`); + } + if (updated.length > 0) { + const files = updated.map((r) => chalk.cyan(r.filename)).join(' and '); + parts.push(`${files} updated`); + } + + clack.log.success(parts.join(', ') + '.'); + clack.log.success('Added project-scoped Sentry MCP configuration.'); + clack.log.info( + chalk.dim( + hasOpenCode + ? 'Note: You may need to reload your editor or restart OpenCode for MCP changes to take effect.' + : 'Note: You may need to reload your editor for MCP changes to take effect.', + ), + ); + } } diff --git a/test/utils/clack/mcp-config.test.ts b/test/utils/clack/mcp-config.test.ts index 2bbc58ff7..9fc388faa 100644 --- a/test/utils/clack/mcp-config.test.ts +++ b/test/utils/clack/mcp-config.test.ts @@ -134,10 +134,10 @@ describe('mcp-config', () => { ); expect(clack.log.success).toHaveBeenCalledWith( - expect.stringContaining('.cursor/mcp.json'), + expect.stringContaining('.cursor/mcp.json created'), ); expect(clack.log.success).toHaveBeenCalledWith( - 'Added project-scoped Sentry MCP configuration for Cursor.', + 'Added project-scoped Sentry MCP configuration.', ); expect(clack.log.info).toHaveBeenCalledWith( expect.stringContaining('reload your editor'), @@ -249,7 +249,7 @@ describe('mcp-config', () => { expect(writtenContent.mcpServers).toHaveProperty('Sentry'); expect(clack.log.success).toHaveBeenCalledWith( - 'Updated .cursor/mcp.json', + expect.stringContaining('.cursor/mcp.json updated'), ); }); @@ -289,7 +289,7 @@ describe('mcp-config', () => { expect(writtenContent.servers?.Sentry).toHaveProperty('type', 'http'); expect(clack.log.success).toHaveBeenCalledWith( - 'Updated .vscode/mcp.json', + expect.stringContaining('.vscode/mcp.json updated'), ); }); @@ -326,7 +326,9 @@ describe('mcp-config', () => { expect(writtenContent.mcpServers).toHaveProperty('OtherServer'); expect(writtenContent.mcpServers).toHaveProperty('Sentry'); - expect(clack.log.success).toHaveBeenCalledWith('Updated .mcp.json'); + expect(clack.log.success).toHaveBeenCalledWith( + expect.stringContaining('.mcp.json updated'), + ); }); it('should configure for OpenCode when selected', async () => { @@ -367,10 +369,10 @@ describe('mcp-config', () => { expect(writtenContent.mcp?.Sentry?.oauth).toEqual({}); expect(clack.log.success).toHaveBeenCalledWith( - expect.stringContaining('opencode.json'), + expect.stringContaining('opencode.json created'), ); expect(clack.log.success).toHaveBeenCalledWith( - 'Added project-scoped Sentry MCP configuration for OpenCode.', + 'Added project-scoped Sentry MCP configuration.', ); expect(clack.log.info).toHaveBeenCalledWith( expect.stringContaining('restart OpenCode'), @@ -412,7 +414,9 @@ describe('mcp-config', () => { expect(writtenContent.mcp).toHaveProperty('Sentry'); expect(writtenContent.mcp?.Sentry).toHaveProperty('type', 'remote'); - expect(clack.log.success).toHaveBeenCalledWith('Updated opencode.json'); + expect(clack.log.success).toHaveBeenCalledWith( + expect.stringContaining('opencode.json updated'), + ); }); it('should handle file write errors gracefully for OpenCode', async () => { @@ -649,7 +653,7 @@ describe('mcp-config', () => { expect(writtenContent.servers).toHaveProperty('Sentry'); expect(clack.log.success).toHaveBeenCalledWith( - 'Updated .vscode/mcp.json', + expect.stringContaining('.vscode/mcp.json updated'), ); }); @@ -959,15 +963,18 @@ describe('mcp-config', () => { 'utf8', ); - // Should show success messages for each (twice per editor: filename + editor-specific message) + // Should show consolidated success message for all created files expect(clack.log.success).toHaveBeenCalledWith( - 'Added project-scoped Sentry MCP configuration for Cursor.', + expect.stringContaining('.cursor/mcp.json'), ); expect(clack.log.success).toHaveBeenCalledWith( - 'Added project-scoped Sentry MCP configuration for VS Code.', + expect.stringContaining('.vscode/mcp.json'), + ); + expect(clack.log.success).toHaveBeenCalledWith( + expect.stringContaining('.mcp.json'), ); expect(clack.log.success).toHaveBeenCalledWith( - 'Added project-scoped Sentry MCP configuration for Claude Code.', + 'Added project-scoped Sentry MCP configuration.', ); });