Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 61 additions & 59 deletions src/utils/clack/mcp-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const file = path.join(process.cwd(), '.cursor', 'mcp.json');
): Promise<McpConfigResult> {
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;
Expand All @@ -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');
}
Expand All @@ -175,18 +178,16 @@ async function addCursorMcpConfig(
async function addVsCodeMcpConfig(
orgSlug?: string,
projectSlug?: string,
): Promise<void> {
const file = path.join(process.cwd(), '.vscode', 'mcp.json');
): Promise<McpConfigResult> {
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;
Expand All @@ -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');
}
Expand All @@ -205,16 +206,16 @@ async function addVsCodeMcpConfig(
async function addClaudeCodeMcpConfig(
orgSlug?: string,
projectSlug?: string,
): Promise<void> {
const file = path.join(process.cwd(), '.mcp.json');
): Promise<McpConfigResult> {
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;
Expand All @@ -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');
}
Expand All @@ -232,16 +233,16 @@ async function addClaudeCodeMcpConfig(
async function addOpenCodeMcpConfig(
orgSlug?: string,
projectSlug?: string,
): Promise<void> {
const file = path.join(process.cwd(), 'opencode.json');
): Promise<McpConfigResult> {
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;
Expand All @@ -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');
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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.',
),
);
}
}
33 changes: 20 additions & 13 deletions test/utils/clack/mcp-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
);
});

Expand Down Expand Up @@ -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'),
);
});

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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'),
);
});

Expand Down Expand Up @@ -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.',
);
});

Expand Down
Loading