mcp-jira-stdio 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +364 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2227 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
- package/scripts/setup-mcp-config.js +250 -0
- package/scripts/test-connection.cjs +89 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const rl = readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Colors for terminal output
|
|
19
|
+
const colors = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bright: '\x1b[1m',
|
|
22
|
+
green: '\x1b[32m',
|
|
23
|
+
yellow: '\x1b[33m',
|
|
24
|
+
blue: '\x1b[34m',
|
|
25
|
+
red: '\x1b[31m',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function question(query) {
|
|
29
|
+
return new Promise((resolve) => rl.question(query, resolve));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function detectNodePath() {
|
|
33
|
+
try {
|
|
34
|
+
// First try to get current Node.js path
|
|
35
|
+
const nodeVersion = process.version;
|
|
36
|
+
console.log(`${colors.green}✓ Detected Node.js version: ${nodeVersion}${colors.reset}`);
|
|
37
|
+
|
|
38
|
+
// Check if using nvm
|
|
39
|
+
const nvmDir = process.env.NVM_DIR;
|
|
40
|
+
if (nvmDir) {
|
|
41
|
+
// Try to get the current version from nvm
|
|
42
|
+
try {
|
|
43
|
+
const currentVersion = execSync('nvm current', { encoding: 'utf8' }).trim();
|
|
44
|
+
const nodePath = path.join(nvmDir, 'versions', 'node', currentVersion, 'bin', 'node');
|
|
45
|
+
if (fs.existsSync(nodePath)) {
|
|
46
|
+
console.log(`${colors.green}✓ Using nvm Node.js at: ${nodePath}${colors.reset}`);
|
|
47
|
+
return nodePath;
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Use the current Node.js executable path
|
|
53
|
+
console.log(`${colors.green}✓ Using Node.js at: ${process.execPath}${colors.reset}`);
|
|
54
|
+
return process.execPath;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log(`${colors.yellow}⚠️ Could not detect Node.js path, using default${colors.reset}`);
|
|
57
|
+
return 'node';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main() {
|
|
62
|
+
console.log(`${colors.bright}${colors.blue}🚀 MCP Jira Configuration Setup${colors.reset}\n`);
|
|
63
|
+
|
|
64
|
+
// Get project path
|
|
65
|
+
const projectPath = path.resolve(__dirname, '..');
|
|
66
|
+
const distIndexPath = path.join(projectPath, 'dist', 'index.js');
|
|
67
|
+
|
|
68
|
+
// Check if we need to build first
|
|
69
|
+
const srcIndexPath = path.join(projectPath, 'src', 'index.ts');
|
|
70
|
+
if (!fs.existsSync(distIndexPath) && fs.existsSync(srcIndexPath)) {
|
|
71
|
+
console.log(`${colors.yellow}⚠️ Dist file not found. Building project...${colors.reset}`);
|
|
72
|
+
try {
|
|
73
|
+
execSync('npm run build', { cwd: projectPath, stdio: 'inherit' });
|
|
74
|
+
console.log(`${colors.green}✓ Build completed${colors.reset}\n`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.log(
|
|
77
|
+
`${colors.red}❌ Build failed. Please run 'npm run build' manually${colors.reset}`
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(distIndexPath)) {
|
|
84
|
+
console.log(`${colors.red}❌ Dist file not found at: ${distIndexPath}${colors.reset}`);
|
|
85
|
+
console.log(`${colors.yellow}Please run 'npm run build' first${colors.reset}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`${colors.green}✓ Found server dist at: ${distIndexPath}${colors.reset}\n`);
|
|
90
|
+
|
|
91
|
+
// Get Jira credentials
|
|
92
|
+
console.log(`${colors.bright}Jira Setup:${colors.reset}`);
|
|
93
|
+
let baseUrl = await question('Jira Base URL (e.g., https://your-instance.atlassian.net): ');
|
|
94
|
+
baseUrl = baseUrl.trim().replace(/\/$/, '');
|
|
95
|
+
const jiraEmail = await question('Jira Email: ');
|
|
96
|
+
const jiraApiToken = await question('Jira API Token: ');
|
|
97
|
+
|
|
98
|
+
// Detect Node.js path
|
|
99
|
+
const nodePath = detectNodePath();
|
|
100
|
+
|
|
101
|
+
// Ask user about Node.js configuration
|
|
102
|
+
console.log(`\n${colors.bright}Node.js Configuration:${colors.reset}`);
|
|
103
|
+
console.log(`1. Use detected Node.js: ${nodePath}`);
|
|
104
|
+
console.log(`2. Specify custom Node.js path`);
|
|
105
|
+
console.log(`3. Use system default (node)`);
|
|
106
|
+
|
|
107
|
+
const nodeChoice = await question('\nSelect option (1-3): ');
|
|
108
|
+
let finalNodePath = nodePath;
|
|
109
|
+
|
|
110
|
+
switch (nodeChoice) {
|
|
111
|
+
case '2':
|
|
112
|
+
finalNodePath = await question('Enter full path to Node.js executable: ');
|
|
113
|
+
if (!fs.existsSync(finalNodePath)) {
|
|
114
|
+
console.log(`${colors.yellow}⚠️ Path not found, using detected path${colors.reset}`);
|
|
115
|
+
finalNodePath = nodePath;
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case '3':
|
|
119
|
+
finalNodePath = 'node';
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create MCP config
|
|
124
|
+
const mcpConfig = {
|
|
125
|
+
mcpServers: {
|
|
126
|
+
jira: {
|
|
127
|
+
command: finalNodePath,
|
|
128
|
+
args: [distIndexPath],
|
|
129
|
+
env: {
|
|
130
|
+
JIRA_BASE_URL: baseUrl,
|
|
131
|
+
JIRA_EMAIL: jiraEmail,
|
|
132
|
+
JIRA_API_TOKEN: jiraApiToken,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Determine config path
|
|
139
|
+
const platform = os.platform();
|
|
140
|
+
let configPath;
|
|
141
|
+
let configDir;
|
|
142
|
+
|
|
143
|
+
if (platform === 'darwin') {
|
|
144
|
+
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
|
|
145
|
+
configPath = path.join(configDir, 'claude_desktop_config.json');
|
|
146
|
+
} else if (platform === 'win32') {
|
|
147
|
+
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude');
|
|
148
|
+
configPath = path.join(configDir, 'claude_desktop_config.json');
|
|
149
|
+
} else {
|
|
150
|
+
configDir = path.join(os.homedir(), '.config', 'claude');
|
|
151
|
+
configPath = path.join(configDir, 'claude_desktop_config.json');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`${colors.bright}Configuration Options:${colors.reset}`);
|
|
155
|
+
console.log('1. Save to Claude Desktop config');
|
|
156
|
+
console.log('2. Display config (copy manually)');
|
|
157
|
+
console.log('3. Save to custom file');
|
|
158
|
+
|
|
159
|
+
const choice = await question('\nSelect option (1-3): ');
|
|
160
|
+
|
|
161
|
+
switch (choice) {
|
|
162
|
+
case '1':
|
|
163
|
+
// Check if config exists
|
|
164
|
+
let existingConfig = {};
|
|
165
|
+
if (fs.existsSync(configPath)) {
|
|
166
|
+
try {
|
|
167
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
168
|
+
if (existingConfig.mcpServers && existingConfig.mcpServers.jira) {
|
|
169
|
+
const overwrite = await question(
|
|
170
|
+
`\n${colors.yellow}⚠️ 'jira' server already exists. Overwrite? (y/n): ${colors.reset}`
|
|
171
|
+
);
|
|
172
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
173
|
+
console.log('Cancelled.');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
delete existingConfig.mcpServers.jira;
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.log(
|
|
180
|
+
`${colors.yellow}⚠️ Could not read existing config, will create new one${colors.reset}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Merge configs
|
|
186
|
+
const finalConfig = {
|
|
187
|
+
...existingConfig,
|
|
188
|
+
mcpServers: {
|
|
189
|
+
...existingConfig.mcpServers,
|
|
190
|
+
...mcpConfig.mcpServers,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (!fs.existsSync(configDir)) {
|
|
195
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fs.writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
|
|
199
|
+
console.log(`\n${colors.green}✅ Configuration saved to: ${configPath}${colors.reset}`);
|
|
200
|
+
console.log(`\n${colors.yellow}⚠️ Restart Claude Desktop to apply changes${colors.reset}`);
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
case '2':
|
|
204
|
+
console.log(
|
|
205
|
+
`\n${colors.bright}Copy this configuration to your Claude Desktop config:${colors.reset}\n`
|
|
206
|
+
);
|
|
207
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case '3':
|
|
211
|
+
const customPath = await question('Enter file path: ');
|
|
212
|
+
fs.writeFileSync(customPath, JSON.stringify(mcpConfig, null, 2));
|
|
213
|
+
console.log(`\n${colors.green}✅ Configuration saved to: ${customPath}${colors.reset}`);
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
default:
|
|
217
|
+
console.log(`${colors.red}Invalid option${colors.reset}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Show next steps
|
|
221
|
+
console.log(`\n${colors.bright}${colors.blue}Next Steps:${colors.reset}`);
|
|
222
|
+
console.log(`1. Restart Claude Desktop`);
|
|
223
|
+
console.log(
|
|
224
|
+
`2. Test Jira connection: ${colors.bright}node scripts/test-connection.cjs${colors.reset}`
|
|
225
|
+
);
|
|
226
|
+
console.log(`3. In Claude, try: "List my Jira projects"`);
|
|
227
|
+
|
|
228
|
+
// Create .env file option
|
|
229
|
+
const createEnv = await question(
|
|
230
|
+
`\n${colors.yellow}Create .env file for development? (y/n): ${colors.reset}`
|
|
231
|
+
);
|
|
232
|
+
if (createEnv.toLowerCase() === 'y') {
|
|
233
|
+
const envContent = `# Jira configuration
|
|
234
|
+
JIRA_BASE_URL=${baseUrl}
|
|
235
|
+
JIRA_EMAIL=${jiraEmail}
|
|
236
|
+
JIRA_API_TOKEN=${jiraApiToken}
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
const envPath = path.join(projectPath, '.env');
|
|
240
|
+
fs.writeFileSync(envPath, envContent);
|
|
241
|
+
console.log(`\n${colors.green}✅ Created .env file for development${colors.reset}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
rl.close();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main().catch((error) => {
|
|
248
|
+
console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Simple test script to verify Jira connection
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
// Load environment variables
|
|
7
|
+
require('dotenv').config();
|
|
8
|
+
|
|
9
|
+
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
|
|
10
|
+
const JIRA_EMAIL = process.env.JIRA_EMAIL;
|
|
11
|
+
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
|
|
12
|
+
|
|
13
|
+
console.log('Testing Jira Connection...');
|
|
14
|
+
console.log('========================');
|
|
15
|
+
console.log(`Base URL: ${JIRA_BASE_URL}`);
|
|
16
|
+
console.log(`Email: ${JIRA_EMAIL}`);
|
|
17
|
+
console.log(`Token: ***${JIRA_API_TOKEN?.slice(-4) || 'MISSING'}`);
|
|
18
|
+
console.log('========================\n');
|
|
19
|
+
|
|
20
|
+
// Parse URL
|
|
21
|
+
const url = new URL(`${JIRA_BASE_URL}/rest/api/3/myself`);
|
|
22
|
+
|
|
23
|
+
// Create auth string (Basic auth)
|
|
24
|
+
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
|
|
25
|
+
|
|
26
|
+
const options = {
|
|
27
|
+
hostname: url.hostname,
|
|
28
|
+
path: url.pathname,
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Basic ${auth}`,
|
|
32
|
+
Accept: 'application/json',
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
console.log(`Making request to: ${url.toString()}`);
|
|
38
|
+
|
|
39
|
+
const req = https.request(options, (res) => {
|
|
40
|
+
console.log(`\nResponse Status: ${res.statusCode}`);
|
|
41
|
+
|
|
42
|
+
let data = '';
|
|
43
|
+
|
|
44
|
+
res.on('data', (chunk) => {
|
|
45
|
+
data += chunk;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
res.on('end', () => {
|
|
49
|
+
try {
|
|
50
|
+
const json = JSON.parse(data);
|
|
51
|
+
|
|
52
|
+
if (res.statusCode === 200) {
|
|
53
|
+
console.log('\n✅ SUCCESS! Connection working!');
|
|
54
|
+
console.log('========================');
|
|
55
|
+
console.log('User Details:');
|
|
56
|
+
console.log(`- Display Name: ${json.displayName}`);
|
|
57
|
+
console.log(`- Email: ${json.emailAddress}`);
|
|
58
|
+
console.log(`- Account ID: ${json.accountId}`);
|
|
59
|
+
console.log(`- Active: ${json.active}`);
|
|
60
|
+
console.log('========================');
|
|
61
|
+
|
|
62
|
+
if (json.emailAddress !== JIRA_EMAIL) {
|
|
63
|
+
console.log('\n⚠️ WARNING: Email mismatch!');
|
|
64
|
+
console.log(`- .env email: ${JIRA_EMAIL}`);
|
|
65
|
+
console.log(`- Jira email: ${json.emailAddress}`);
|
|
66
|
+
console.log('\nYou should update JIRA_EMAIL in your .env file to: ' + json.emailAddress);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
console.log('\n❌ FAILED! Authentication error');
|
|
70
|
+
console.log('Response:', json);
|
|
71
|
+
|
|
72
|
+
if (res.statusCode === 401) {
|
|
73
|
+
console.log('\nPossible issues:');
|
|
74
|
+
console.log('1. Wrong email address (check if it matches your Jira account)');
|
|
75
|
+
console.log('2. Invalid or expired API token');
|
|
76
|
+
console.log('3. Token needs to be regenerated');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.log('Response body:', data);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
req.on('error', (e) => {
|
|
86
|
+
console.error(`\n❌ Network Error: ${e.message}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
req.end();
|