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.
- package/auth/jenkins-auth.js +1 -1
- package/auth/slack-auth.js +76 -10
- package/mcp-server.js +14 -21
- package/package.json +1 -1
package/auth/jenkins-auth.js
CHANGED
package/auth/slack-auth.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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: '
|
|
29
|
-
description: '
|
|
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💡
|
|
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 '
|
|
148
|
-
|
|
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(
|
|
153
|
-
return text(`✅ Slack
|
|
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(
|
|
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 **
|
|
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💡
|
|
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