@sentry/wizard
Version:
Sentry wizard helping you to configure your project
427 lines • 16.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.offerProjectScopedMcpConfig = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const childProcess = __importStar(require("child_process"));
const Sentry = __importStar(require("@sentry/node"));
const chalk_1 = __importDefault(require("chalk"));
// @ts-expect-error - clack is ESM and TS complains about that. It works though
const clack = __importStar(require("@clack/prompts"));
const index_1 = require("./index");
const SENTRY_MCP_BASE_URL = 'https://mcp.sentry.dev/mcp';
/**
* Constructs the MCP URL with optional org and project slugs
*/
function getMcpUrl(orgSlug, projectSlug) {
if (orgSlug && projectSlug) {
return `${SENTRY_MCP_BASE_URL}/${orgSlug}/${projectSlug}`;
}
return SENTRY_MCP_BASE_URL;
}
function ensureDir(dirpath) {
fs.mkdirSync(dirpath, { recursive: true });
}
async function readJsonIfExists(filepath) {
try {
const txt = await fs.promises.readFile(filepath, 'utf8');
return JSON.parse(txt);
}
catch {
return null;
}
}
async function writeJson(filepath, obj) {
ensureDir(path.dirname(filepath));
await fs.promises.writeFile(filepath, JSON.stringify(obj, null, 2), 'utf8');
}
function getCursorMcpJsonSnippet(orgSlug, projectSlug) {
const obj = {
mcpServers: {
Sentry: {
url: getMcpUrl(orgSlug, projectSlug),
},
},
};
return JSON.stringify(obj, null, 2);
}
function getVsCodeMcpJsonSnippet(orgSlug, projectSlug) {
const obj = {
servers: {
Sentry: {
url: getMcpUrl(orgSlug, projectSlug),
type: 'http',
},
},
};
return JSON.stringify(obj, null, 2);
}
function getClaudeCodeMcpJsonSnippet(orgSlug, projectSlug) {
const obj = {
mcpServers: {
Sentry: {
url: getMcpUrl(orgSlug, projectSlug),
},
},
};
return JSON.stringify(obj, null, 2);
}
function getJetBrainsMcpJsonSnippet(orgSlug, projectSlug) {
const obj = {
mcpServers: {
Sentry: {
url: getMcpUrl(orgSlug, projectSlug),
},
},
};
return JSON.stringify(obj, null, 2);
}
function getGenericMcpJsonSnippet(orgSlug, projectSlug) {
const obj = {
mcpServers: {
Sentry: {
url: getMcpUrl(orgSlug, projectSlug),
},
},
};
return JSON.stringify(obj, null, 2);
}
async function addCursorMcpConfig(orgSlug, projectSlug) {
const file = path.join(process.cwd(), '.cursor', 'mcp.json');
const existing = await readJsonIfExists(file);
if (!existing) {
await writeJson(file, JSON.parse(getCursorMcpJsonSnippet(orgSlug, projectSlug)));
clack.log.success(chalk_1.default.cyan(path.join('.cursor', 'mcp.json')) + ' created.');
return;
}
try {
const updated = { ...existing };
updated.mcpServers = updated.mcpServers || {};
updated.mcpServers['Sentry'] = {
url: getMcpUrl(orgSlug, projectSlug),
};
await writeJson(file, updated);
clack.log.success('Updated .cursor/mcp.json');
}
catch {
throw new Error('Failed to update .cursor/mcp.json');
}
}
async function addVsCodeMcpConfig(orgSlug, projectSlug) {
const file = path.join(process.cwd(), '.vscode', 'mcp.json');
const existing = await readJsonIfExists(file);
if (!existing) {
await writeJson(file, JSON.parse(getVsCodeMcpJsonSnippet(orgSlug, projectSlug)));
clack.log.success(chalk_1.default.cyan(path.join('.vscode', 'mcp.json')) + ' created.');
return;
}
try {
const updated = { ...existing };
updated.servers = updated.servers || {};
updated.servers['Sentry'] = {
url: getMcpUrl(orgSlug, projectSlug),
type: 'http',
};
await writeJson(file, updated);
clack.log.success('Updated .vscode/mcp.json');
}
catch {
throw new Error('Failed to update .vscode/mcp.json');
}
}
async function addClaudeCodeMcpConfig(orgSlug, projectSlug) {
const file = path.join(process.cwd(), '.mcp.json');
const existing = await readJsonIfExists(file);
if (!existing) {
await writeJson(file, JSON.parse(getClaudeCodeMcpJsonSnippet(orgSlug, projectSlug)));
clack.log.success(chalk_1.default.cyan('.mcp.json') + ' created.');
return;
}
try {
const updated = { ...existing };
updated.mcpServers = updated.mcpServers || {};
updated.mcpServers['Sentry'] = {
url: getMcpUrl(orgSlug, projectSlug),
};
await writeJson(file, updated);
clack.log.success('Updated .mcp.json');
}
catch {
throw new Error('Failed to update .mcp.json');
}
}
/**
* Copies text to clipboard across different platforms
*/
async function copyToClipboard(text) {
try {
const platform = process.platform;
let command;
if (platform === 'darwin') {
command = 'pbcopy';
}
else if (platform === 'win32') {
command = 'clip';
}
else {
// Linux
command = 'xclip -selection clipboard';
}
const proc = childProcess.spawn(command, [], { shell: true });
proc.stdin.write(text);
proc.stdin.end();
return new Promise((resolve) => {
proc.on('close', (code) => {
resolve(code === 0);
});
proc.on('error', () => {
resolve(false);
});
});
}
catch {
return false;
}
}
/**
* Shows MCP configuration for JetBrains IDEs with copy-to-clipboard option
*/
async function showJetBrainsMcpConfig(orgSlug, projectSlug) {
const configSnippet = getJetBrainsMcpJsonSnippet(orgSlug, projectSlug);
clack.log.info(chalk_1.default.cyan('For JetBrains IDEs (WebStorm, IntelliJ IDEA, PyCharm, etc.):'));
clack.log.info(chalk_1.default.dim("Add the following configuration to your IDE's MCP settings.\n" +
'See: https://www.jetbrains.com/help/webstorm/mcp-server.html'));
// Display the configuration
// eslint-disable-next-line no-console
console.log('\n' + chalk_1.default.green(configSnippet) + '\n');
// Ask if user wants to copy to clipboard
const shouldCopy = await (0, index_1.abortIfCancelled)(clack.select({
message: 'Copy configuration to clipboard?',
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
initialValue: true,
}));
if (shouldCopy) {
const copied = await copyToClipboard(configSnippet);
if (copied) {
clack.log.success('Configuration copied to clipboard!');
Sentry.setTag('mcp-clipboard-copy', 'success');
}
else {
clack.log.warn('Failed to copy to clipboard. Please copy the configuration above manually.');
Sentry.setTag('mcp-clipboard-copy', 'failed');
}
}
else {
Sentry.setTag('mcp-clipboard-copy', 'declined');
}
clack.log.info(chalk_1.default.dim('Note: You may need to restart your IDE for MCP changes to take effect.'));
}
/**
* Shows generic MCP configuration for unsupported IDEs with copy-to-clipboard option
*/
async function showGenericMcpConfig(orgSlug, projectSlug) {
const configSnippet = getGenericMcpJsonSnippet(orgSlug, projectSlug);
clack.log.info(chalk_1.default.cyan('Generic MCP configuration for your IDE:'));
clack.log.info(chalk_1.default.dim('If your IDE supports MCP servers, you can use the following configuration.\n' +
"Please consult your IDE's documentation for how to add MCP server configurations."));
// Display the configuration
// eslint-disable-next-line no-console
console.log('\n' + chalk_1.default.green(configSnippet) + '\n');
// Ask if user wants to copy to clipboard
const shouldCopy = await (0, index_1.abortIfCancelled)(clack.select({
message: 'Copy configuration to clipboard?',
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
initialValue: true,
}));
if (shouldCopy) {
const copied = await copyToClipboard(configSnippet);
if (copied) {
clack.log.success('Configuration copied to clipboard!');
Sentry.setTag('mcp-clipboard-copy', 'success');
}
else {
clack.log.warn('Failed to copy to clipboard. Please copy the configuration above manually.');
Sentry.setTag('mcp-clipboard-copy', 'failed');
}
}
else {
Sentry.setTag('mcp-clipboard-copy', 'declined');
}
clack.log.info(chalk_1.default.dim('Note: The exact configuration format may vary depending on your IDE.\n' +
"If your IDE doesn't support MCP yet, please check back later or open an issue at:\n" +
'https://github.com/getsentry/sentry-wizard/issues'));
}
/**
* Explains what MCP is and its benefits for Sentry users
*/
async function explainMCP() {
clack.log.info(chalk_1.default.cyan('What is MCP (Model Context Protocol)?'));
clack.log.info(chalk_1.default.dim('MCP is a protocol that allows AI assistants in your IDE to interact with external tools and services.\n\n' +
'The Sentry MCP server enables AI assistants to:\n' +
' • Query and analyze your Sentry issues directly from your IDE\n' +
' • Get context about errors and performance problems\n' +
' • Help debug issues with production data insights\n' +
' • Suggest fixes based on real error patterns\n\n' +
"This makes it easier to fix bugs by bringing Sentry's insights directly into your development workflow.\n\n" +
'Learn more: ' +
chalk_1.default.cyan('https://docs.sentry.io/product/sentry-mcp/')));
// Ask again after explanation
const shouldAddAfterExplanation = await (0, index_1.abortIfCancelled)(clack.select({
message: 'Would you like to configure MCP for your IDE now?',
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false, hint: 'You can add it later anytime' },
],
initialValue: true,
}));
return shouldAddAfterExplanation;
}
/**
* Offers to add a project-scoped MCP server configuration for the Sentry MCP.
* Supports Cursor, VS Code, and Claude Code.
* @param orgSlug - Optional organization slug to include in the MCP URL
* @param projectSlug - Optional project slug to include in the MCP URL
*/
async function offerProjectScopedMcpConfig(orgSlug, projectSlug) {
const initialChoice = await (0, index_1.abortIfCancelled)(clack.select({
message: 'Optionally add a project-scoped MCP server configuration for the Sentry MCP?',
options: [
{ label: 'Yes', value: 'yes' },
{ label: 'No', value: 'no', hint: 'You can add it later anytime' },
{
label: 'What is MCP?',
value: 'explain',
hint: 'Learn about MCP benefits',
},
],
initialValue: 'yes',
}));
let shouldAdd;
if (initialChoice === 'explain') {
Sentry.setTag('mcp-choice', 'explain');
shouldAdd = await explainMCP();
Sentry.setTag('mcp-configured-after-explain', shouldAdd);
}
else {
shouldAdd = initialChoice === 'yes';
Sentry.setTag('mcp-choice', initialChoice);
}
if (!shouldAdd) {
Sentry.setTag('mcp-configured', false);
return;
}
Sentry.setTag('mcp-configured', true);
const editor = await (0, index_1.abortIfCancelled)(clack.select({
message: 'Which editor do you want to configure?',
options: [
{ value: 'cursor', label: 'Cursor (project .cursor/mcp.json)' },
{ value: 'vscode', label: 'VS Code (project .vscode/mcp.json)' },
{ value: 'claudeCode', label: 'Claude Code (project .mcp.json)' },
{
value: 'jetbrains',
label: 'JetBrains IDE (WebStorm, IntelliJ IDEA, PyCharm, etc.)',
hint: 'Manual configuration required',
},
{
value: 'other',
label: 'I use a different IDE',
hint: "We'll show you the configuration to copy",
},
],
}));
// Track which editor was selected
Sentry.setTag('mcp-editor', editor);
try {
switch (editor) {
case 'cursor':
await addCursorMcpConfig(orgSlug, projectSlug);
clack.log.success('Added project-scoped Sentry MCP configuration.');
clack.log.info(chalk_1.default.dim('Note: You may need to reload your editor for MCP changes to take effect.'));
Sentry.setTag('mcp-config-success', true);
break;
case 'vscode':
await addVsCodeMcpConfig(orgSlug, projectSlug);
clack.log.success('Added project-scoped Sentry MCP configuration.');
clack.log.info(chalk_1.default.dim('Note: You may need to reload your editor for MCP changes to take effect.'));
Sentry.setTag('mcp-config-success', true);
break;
case 'claudeCode':
await addClaudeCodeMcpConfig(orgSlug, projectSlug);
clack.log.success('Added project-scoped Sentry MCP configuration.');
clack.log.info(chalk_1.default.dim('Note: You may need to reload your editor for MCP changes to take effect.'));
Sentry.setTag('mcp-config-success', true);
break;
case 'jetbrains':
await showJetBrainsMcpConfig(orgSlug, projectSlug);
Sentry.setTag('mcp-config-success', true);
Sentry.setTag('mcp-config-manual', true);
break;
case 'other':
await showGenericMcpConfig(orgSlug, projectSlug);
Sentry.setTag('mcp-config-success', true);
Sentry.setTag('mcp-config-manual', true);
break;
}
}
catch (e) {
Sentry.setTag('mcp-config-success', false);
Sentry.setTag('mcp-config-fallback', true);
clack.log.warn(chalk_1.default.yellow('Failed to write MCP config automatically. Please copy/paste the snippet below into your project config file.'));
// Fallback: show per-editor instructions
if (editor === 'cursor') {
await (0, index_1.showCopyPasteInstructions)({
filename: path.join('.cursor', 'mcp.json'),
codeSnippet: getCursorMcpJsonSnippet(orgSlug, projectSlug),
hint: 'create the file if it does not exist',
});
}
else if (editor === 'vscode') {
await (0, index_1.showCopyPasteInstructions)({
filename: path.join('.vscode', 'mcp.json'),
codeSnippet: getVsCodeMcpJsonSnippet(orgSlug, projectSlug),
hint: 'create the file if it does not exist',
});
}
else if (editor === 'claudeCode') {
await (0, index_1.showCopyPasteInstructions)({
filename: '.mcp.json',
codeSnippet: getClaudeCodeMcpJsonSnippet(orgSlug, projectSlug),
hint: 'create the file if it does not exist',
});
}
}
}
exports.offerProjectScopedMcpConfig = offerProjectScopedMcpConfig;
//# sourceMappingURL=mcp-config.js.map