jenkins-slack-mcp 2.0.1 → 2.1.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.
@@ -1,5 +1,5 @@
1
1
  const axios = require('axios');
2
- e readconst open = require('open');
2
+ const open = require('open');
3
3
 
4
4
  async function openJenkinsTokenPage(baseUrl) {
5
5
  const tokenUrl = `${baseUrl}/me/configure`;
@@ -1,22 +1,88 @@
1
1
  const axios = require('axios');
2
+ const express = require('express');
3
+ const open = require('open');
4
+ const crypto = require('crypto');
5
+
6
+ const SCOPES = 'chat:write,users:read,im:write';
7
+
8
+ async function slackOAuthLogin(clientId, clientSecret) {
9
+ if (!clientId || !clientSecret) {
10
+ throw new Error('Set SLACK_CLIENT_ID and SLACK_CLIENT_SECRET environment variables');
11
+ }
12
+
13
+ return new Promise((resolve, reject) => {
14
+ const app = express();
15
+ const state = crypto.randomBytes(16).toString('hex');
16
+ let server;
17
+
18
+ const timeout = setTimeout(() => {
19
+ server?.close();
20
+ reject(new Error('OAuth timed out after 2 minutes'));
21
+ }, 120000);
22
+
23
+ app.get('/slack/callback', async (req, res) => {
24
+ try {
25
+ if (req.query.state !== state) {
26
+ res.status(400).send('Invalid state');
27
+ return;
28
+ }
29
+ if (req.query.error) {
30
+ res.send(`<h2>❌ ${req.query.error}</h2>`);
31
+ reject(new Error(req.query.error));
32
+ return;
33
+ }
34
+
35
+ // Exchange code for token
36
+ const tokenResp = await axios.post('https://slack.com/api/oauth.v2.access', null, {
37
+ params: {
38
+ client_id: clientId,
39
+ client_secret: clientSecret,
40
+ code: req.query.code,
41
+ redirect_uri: `http://localhost:${server.address().port}/slack/callback`,
42
+ },
43
+ });
44
+
45
+ if (!tokenResp.data.ok) {
46
+ res.send(`<h2>❌ ${tokenResp.data.error}</h2>`);
47
+ reject(new Error(tokenResp.data.error));
48
+ return;
49
+ }
50
+
51
+ const { access_token: botToken, authed_user } = tokenResp.data;
52
+ const userId = authed_user?.id;
53
+
54
+ res.send('<h2>✅ Slack connected! You can close this tab.</h2>');
55
+ clearTimeout(timeout);
56
+ server.close();
57
+ resolve({ botToken, userId });
58
+ } catch (err) {
59
+ res.status(500).send(`<h2>❌ ${err.message}</h2>`);
60
+ clearTimeout(timeout);
61
+ server.close();
62
+ reject(err);
63
+ }
64
+ });
65
+
66
+ server = app.listen(0, () => {
67
+ const port = server.address().port;
68
+ const redirectUri = encodeURIComponent(`http://localhost:${port}/slack/callback`);
69
+ const url = `https://slack.com/oauth/v2/authorize?client_id=${clientId}&scope=${SCOPES}&user_scope=&state=${state}&redirect_uri=${redirectUri}`;
70
+ open(url);
71
+ });
72
+ });
73
+ }
2
74
 
3
75
  async function sendSlackDM(botToken, userId, message) {
4
- // Open DM channel with user
5
- const openResp = await axios.post('https://slack.com/api/conversations.open',
76
+ const openResp = await axios.post('https://slack.com/api/conversations.open',
6
77
  { users: userId },
7
78
  { headers: { Authorization: `Bearer ${botToken}`, 'Content-Type': 'application/json' } }
8
79
  );
9
-
10
80
  if (!openResp.data.ok) throw new Error(`Slack DM open failed: ${openResp.data.error}`);
11
-
12
- const channelId = openResp.data.channel.id;
13
-
14
- // Send message
81
+
15
82
  const msgResp = await axios.post('https://slack.com/api/chat.postMessage',
16
- { channel: channelId, text: message },
83
+ { channel: openResp.data.channel.id, text: message },
17
84
  { headers: { Authorization: `Bearer ${botToken}`, 'Content-Type': 'application/json' } }
18
85
  );
19
-
20
86
  if (!msgResp.data.ok) throw new Error(`Slack message failed: ${msgResp.data.error}`);
21
87
  }
22
88
 
@@ -28,4 +94,4 @@ async function postToChannel(botToken, channel, message) {
28
94
  if (!resp.data.ok) throw new Error(`Slack post failed: ${resp.data.error}`);
29
95
  }
30
96
 
31
- module.exports = { sendSlackDM, postToChannel };
97
+ module.exports = { slackOAuthLogin, sendSlackDM, postToChannel };
package/mcp-server.js CHANGED
@@ -4,9 +4,9 @@ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio
4
4
  const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
5
5
  const { getCredentials, saveJenkinsCredentials, saveSlackConfig, saveJobs, getJobs, clearAll } = require('./auth/credentials');
6
6
  const { openJenkinsTokenPage, validateAndFetchUser, fetchAllJobs, fetchJobParams, triggerBuild } = require('./auth/jenkins-auth');
7
- const { sendSlackDM, postToChannel } = require('./auth/slack-auth');
7
+ const { slackOAuthLogin, sendSlackDM, postToChannel } = require('./auth/slack-auth');
8
8
 
9
- const server = new Server({ name: 'jenkins-slack-mcp', version: '1.0.4' }, { capabilities: { tools: {} } });
9
+ const server = new Server({ name: 'jenkins-slack-mcp', version: '2.1.0' }, { capabilities: { tools: {} } });
10
10
 
11
11
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
12
12
  tools: [
@@ -25,16 +25,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
25
25
  },
26
26
  },
27
27
  {
28
- name: 'setup_slack',
29
- description: 'Configure Slack notifications. Set bot token and your Slack User ID to receive DMs on build trigger.',
30
- inputSchema: {
31
- type: 'object',
32
- properties: {
33
- botToken: { type: 'string', description: 'Slack Bot OAuth Token (xoxb-...)' },
34
- userId: { type: 'string', description: 'Your Slack User ID (found in Profile → ... → Copy Member ID)' },
35
- },
36
- required: ['botToken', 'userId'],
37
- },
28
+ name: 'login_slack',
29
+ description: 'Login to Slack via OAuth. Opens browser for authorization. Requires SLACK_CLIENT_ID and SLACK_CLIENT_SECRET env vars.',
30
+ inputSchema: { type: 'object', properties: {} },
38
31
  },
39
32
  {
40
33
  name: 'status',
@@ -139,20 +132,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
139
132
 
140
133
  const creds = getCredentials();
141
134
  if (!creds.slack) {
142
- msg += `\n\n💡 Set up Slack notifications with **setup_slack** to get DMs on build trigger.`;
135
+ msg += `\n\n💡 Use **login_slack** to connect Slack and get DM notifications on build trigger.`;
143
136
  }
144
137
  return text(msg);
145
138
  }
146
139
 
147
- case 'setup_slack': {
148
- saveSlackConfig({ botToken: args.botToken, userId: args.userId });
140
+ case 'login_slack': {
141
+ const { botToken, userId } = await slackOAuthLogin(process.env.SLACK_CLIENT_ID, process.env.SLACK_CLIENT_SECRET);
142
+ saveSlackConfig({ botToken, userId });
149
143
 
150
- // Test by sending a welcome DM
151
144
  try {
152
- await sendSlackDM(args.botToken, args.userId, '✅ Jenkins-Slack MCP connected! You will receive build notifications here.');
153
- return text(`✅ Slack configured!\n- User ID: ${args.userId}\n- ✉️ Test DM sent to you successfully.`);
145
+ await sendSlackDM(botToken, userId, '✅ Jenkins-Slack MCP connected! You will receive build notifications here.');
146
+ return text(`✅ Slack connected via OAuth!\n- User ID: ${userId}\n- ✉️ Test DM sent successfully.`);
154
147
  } catch (err) {
155
- return text(`⚠️ Slack config saved but test DM failed: ${err.message}\nCheck your bot token and user ID.`);
148
+ return text(`✅ Slack OAuth successful (User: ${userId}) but test DM failed: ${err.message}`);
156
149
  }
157
150
  }
158
151
 
@@ -164,7 +157,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
164
157
  : '❌ Jenkins: not logged in. Use **login_jenkins** to connect.';
165
158
  const slack = creds.slack
166
159
  ? `✅ Slack: User ID ${creds.slack.userId} (DM notifications enabled)`
167
- : '❌ Slack: not configured. Use **setup_slack** to enable notifications.';
160
+ : '❌ Slack: not configured. Use **login_slack** to enable notifications.';
168
161
  return text(`${jenkins}\n${slack}\n📋 Jobs available: ${jobCount}`);
169
162
  }
170
163
 
@@ -267,7 +260,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
267
260
  }
268
261
 
269
262
  if (!creds.slack) {
270
- msg += `\n\n💡 Set up **setup_slack** to get DM notifications on every build.`;
263
+ msg += `\n\n💡 Use **login_slack** to get DM notifications on every build.`;
271
264
  }
272
265
 
273
266
  return text(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jenkins-slack-mcp",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server to trigger Jenkins builds from any IDE. Auto-discovers jobs, browser login, Slack DM notifications.",
5
5
  "main": "mcp-server.js",
6
6
  "bin": {