jenkins-slack-mcp 1.0.1 → 2.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/README.md +133 -48
- package/auth/credentials.js +4 -12
- package/auth/jenkins-auth.js +26 -5
- package/auth/slack-auth.js +25 -66
- package/mcp-server.js +196 -69
- package/package.json +3 -4
- package/server.js +0 -39
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# jenkins-slack-mcp
|
|
2
2
|
|
|
3
|
-
MCP server to trigger Jenkins builds from any IDE
|
|
3
|
+
MCP server to trigger Jenkins builds from any IDE with Slack DM notifications. Auto-discovers all jobs on login — no manual configuration.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install -g
|
|
8
|
+
npm install -g jenkins-slack-mcp
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Register in your IDE
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### Amazon Q
|
|
14
|
+
|
|
15
|
+
`~/.aws/amazonq/mcp.json`:
|
|
14
16
|
```json
|
|
15
17
|
{
|
|
16
18
|
"mcpServers": {
|
|
@@ -23,89 +25,172 @@ npm install -g github:Seetharam1999/jenkins-slack-mcp
|
|
|
23
25
|
}
|
|
24
26
|
```
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
### VS Code
|
|
29
|
+
|
|
30
|
+
`.vscode/mcp.json`:
|
|
27
31
|
```json
|
|
28
32
|
{
|
|
29
33
|
"mcpServers": {
|
|
30
|
-
"jenkins-slack": {
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
"jenkins-slack": { "command": "jenkins-slack-mcp" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Claude Desktop
|
|
40
|
+
|
|
41
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"jenkins-slack": { "command": "jenkins-slack-mcp" }
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
```
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
### Cursor
|
|
51
|
+
|
|
52
|
+
`.cursor/mcp.json`:
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"jenkins-slack": { "command": "jenkins-slack-mcp" }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Without global install
|
|
62
|
+
|
|
38
63
|
```json
|
|
39
64
|
{
|
|
40
65
|
"mcpServers": {
|
|
41
66
|
"jenkins-slack": {
|
|
42
67
|
"command": "npx",
|
|
43
|
-
"args": ["
|
|
68
|
+
"args": ["-y", "jenkins-slack-mcp"]
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
71
|
}
|
|
47
72
|
```
|
|
48
73
|
|
|
49
|
-
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Flow of Work
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
┌─────────────────────────────────────────────────────────┐
|
|
80
|
+
│ 1. login_jenkins (baseUrl only) │
|
|
81
|
+
│ → Opens browser to Jenkins token page │
|
|
82
|
+
│ → User copies API token + build trigger token │
|
|
83
|
+
│ │
|
|
84
|
+
│ 2. login_jenkins (all params) │
|
|
85
|
+
│ → Validates credentials │
|
|
86
|
+
│ → Auto-discovers ALL jobs from Jenkins │
|
|
87
|
+
│ → Stores securely at ~/.jenkins-slack-mcp/ │
|
|
88
|
+
│ │
|
|
89
|
+
│ 3. list_jobs / list_jobs filter="tms" │
|
|
90
|
+
│ → Shows jobs in table with status (✅ ❌ ⏸️) │
|
|
91
|
+
│ │
|
|
92
|
+
│ 4. job_details jobName="my-job" │
|
|
93
|
+
│ → Shows all parameters (name, type, default, │
|
|
94
|
+
│ choices) in table format │
|
|
95
|
+
│ │
|
|
96
|
+
│ 5. trigger_build jobName="my-job" params={...} │
|
|
97
|
+
│ → Triggers build with params │
|
|
98
|
+
│ → Sends Slack DM to you (if configured) │
|
|
99
|
+
│ → Posts to channel (if notifyChannel provided) │
|
|
100
|
+
│ │
|
|
101
|
+
│ (Optional) setup_slack │
|
|
102
|
+
│ → Configure bot token + your Slack User ID │
|
|
103
|
+
│ → Get DM notifications on every build trigger │
|
|
104
|
+
└─────────────────────────────────────────────────────────┘
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Usage
|
|
110
|
+
|
|
111
|
+
### Step 1: Login to Jenkins
|
|
112
|
+
|
|
113
|
+
First time — just provide the URL, browser opens automatically:
|
|
114
|
+
```
|
|
115
|
+
login_jenkins baseUrl="https://jenkins.example.com"
|
|
116
|
+
```
|
|
117
|
+
→ Browser opens Jenkins → Copy your API token and build trigger token.
|
|
50
118
|
|
|
51
|
-
|
|
119
|
+
Then login with full credentials:
|
|
52
120
|
```
|
|
53
121
|
login_jenkins:
|
|
54
|
-
baseUrl: https://
|
|
122
|
+
baseUrl: https://jenkins.example.com
|
|
55
123
|
user: your_username
|
|
56
124
|
apiToken: your_api_token
|
|
57
|
-
buildToken:
|
|
125
|
+
buildToken: your_build_trigger_token
|
|
126
|
+
```
|
|
127
|
+
→ All jobs auto-discovered!
|
|
128
|
+
|
|
129
|
+
### Step 2: Browse Jobs
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
list_jobs # all jobs
|
|
133
|
+
list_jobs filter="tms" # filter by name
|
|
58
134
|
```
|
|
59
135
|
|
|
60
|
-
|
|
136
|
+
Output:
|
|
137
|
+
| # | Job Name | Status |
|
|
138
|
+
|---|----------|--------|
|
|
139
|
+
| 1 | tms-docker-build-new | ✅ Success |
|
|
140
|
+
| 2 | tms-constants | ✅ Success |
|
|
141
|
+
| 3 | TMS-version-update | ✅ Success |
|
|
142
|
+
|
|
143
|
+
### Step 3: Check Parameters
|
|
144
|
+
|
|
61
145
|
```
|
|
62
|
-
|
|
63
|
-
clientId: your_slack_app_client_id
|
|
64
|
-
clientSecret: your_slack_app_client_secret
|
|
146
|
+
job_details jobName="tms-docker-build-new"
|
|
65
147
|
```
|
|
66
|
-
Opens browser → OAuth → done.
|
|
67
148
|
|
|
68
|
-
|
|
149
|
+
Output:
|
|
150
|
+
| Parameter | Type | Default | Choices | Description |
|
|
151
|
+
|-----------|------|---------|---------|-------------|
|
|
152
|
+
| BRANCH | String | main | - | Branch to build |
|
|
153
|
+
|
|
154
|
+
### Step 4: Trigger Build
|
|
155
|
+
|
|
69
156
|
```
|
|
70
|
-
|
|
71
|
-
command: /buildtt
|
|
72
|
-
jobPath: /view/track-and-trace/job/track-and-trace
|
|
73
|
-
name: Track & Trace
|
|
74
|
-
defaultBranch: main
|
|
157
|
+
trigger_build jobName="tms-docker-build-new" params={"BRANCH": "feature/xyz"}
|
|
75
158
|
```
|
|
76
159
|
|
|
77
|
-
###
|
|
160
|
+
### Step 5 (Optional): Setup Slack DMs
|
|
161
|
+
|
|
78
162
|
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
slackChannel: #deployments
|
|
163
|
+
setup_slack:
|
|
164
|
+
botToken: xoxb-your-bot-token
|
|
165
|
+
userId: U0123456789
|
|
83
166
|
```
|
|
84
167
|
|
|
85
|
-
|
|
168
|
+
**How to get your Slack User ID:**
|
|
169
|
+
1. Open Slack → Click your profile picture
|
|
170
|
+
2. Click "Profile" → Click "..." (more)
|
|
171
|
+
3. Click "Copy Member ID"
|
|
86
172
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| `login_slack` | OAuth login to Slack |
|
|
91
|
-
| `status` | Check login status |
|
|
92
|
-
| `whoami` | Get user details from both services |
|
|
93
|
-
| `add_job` | Register a Jenkins job |
|
|
94
|
-
| `list_jobs` | Show registered jobs |
|
|
95
|
-
| `trigger_build` | Build + optional Slack notify |
|
|
96
|
-
| `logout` | Clear stored credentials |
|
|
173
|
+
**How to get bot token:**
|
|
174
|
+
1. Go to https://api.slack.com/apps → Your App
|
|
175
|
+
2. OAuth & Permissions → Bot User OAuth Token (starts with `xoxb-`)
|
|
97
176
|
|
|
98
|
-
|
|
177
|
+
After setup, every `trigger_build` sends you a DM automatically.
|
|
99
178
|
|
|
100
|
-
|
|
179
|
+
---
|
|
101
180
|
|
|
102
|
-
|
|
103
|
-
node server.js
|
|
104
|
-
# Expose with ngrok:
|
|
105
|
-
ngrok http 3001
|
|
106
|
-
```
|
|
181
|
+
## Available Tools
|
|
107
182
|
|
|
108
|
-
|
|
183
|
+
| Tool | Description |
|
|
184
|
+
|------|-------------|
|
|
185
|
+
| `login_jenkins` | Login + auto-discover jobs (opens browser for new users) |
|
|
186
|
+
| `setup_slack` | Configure Slack bot token + User ID for DM notifications |
|
|
187
|
+
| `status` | Check login status + job count |
|
|
188
|
+
| `list_jobs` | All jobs in table format (filterable) |
|
|
189
|
+
| `job_details` | Show parameters for a job |
|
|
190
|
+
| `trigger_build` | Trigger build + Slack DM + channel notify |
|
|
191
|
+
| `refresh_jobs` | Re-fetch jobs from Jenkins |
|
|
192
|
+
| `whoami` | Current user details |
|
|
193
|
+
| `logout` | Clear all credentials |
|
|
109
194
|
|
|
110
195
|
## Credentials
|
|
111
196
|
|
package/auth/credentials.js
CHANGED
|
@@ -22,10 +22,7 @@ function saveConfig(config) {
|
|
|
22
22
|
|
|
23
23
|
function getCredentials() {
|
|
24
24
|
const config = loadConfig();
|
|
25
|
-
return {
|
|
26
|
-
jenkins: config.jenkins || null,
|
|
27
|
-
slack: config.slack || null,
|
|
28
|
-
};
|
|
25
|
+
return { jenkins: config.jenkins || null, slack: config.slack || null };
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
function saveJenkinsCredentials({ baseUrl, user, apiToken, buildToken }) {
|
|
@@ -34,9 +31,9 @@ function saveJenkinsCredentials({ baseUrl, user, apiToken, buildToken }) {
|
|
|
34
31
|
saveConfig(config);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
function
|
|
34
|
+
function saveSlackConfig({ botToken, userId }) {
|
|
38
35
|
const config = loadConfig();
|
|
39
|
-
config.slack = { botToken,
|
|
36
|
+
config.slack = { botToken, userId };
|
|
40
37
|
saveConfig(config);
|
|
41
38
|
}
|
|
42
39
|
|
|
@@ -51,13 +48,8 @@ function getJobs() {
|
|
|
51
48
|
return config.jobs || {};
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
function isConfigured() {
|
|
55
|
-
const creds = getCredentials();
|
|
56
|
-
return !!(creds.jenkins && creds.slack);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
51
|
function clearAll() {
|
|
60
52
|
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
61
53
|
}
|
|
62
54
|
|
|
63
|
-
module.exports = { getCredentials, saveJenkinsCredentials,
|
|
55
|
+
module.exports = { getCredentials, saveJenkinsCredentials, saveSlackConfig, saveJobs, getJobs, clearAll };
|
package/auth/jenkins-auth.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const open = require('open');
|
|
3
|
+
|
|
4
|
+
async function openJenkinsTokenPage(baseUrl) {
|
|
5
|
+
const tokenUrl = `${baseUrl}/me/configure`;
|
|
6
|
+
await open(tokenUrl);
|
|
7
|
+
return tokenUrl;
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
async function validateAndFetchUser(baseUrl, user, apiToken) {
|
|
4
11
|
const resp = await axios.get(`${baseUrl}/me/api/json`, {
|
|
@@ -7,19 +14,33 @@ async function validateAndFetchUser(baseUrl, user, apiToken) {
|
|
|
7
14
|
return { fullName: resp.data.fullName, id: resp.data.id };
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
async function
|
|
11
|
-
const resp = await axios.get(`${baseUrl}/api/json?tree=jobs[name,url,color]`, {
|
|
17
|
+
async function fetchAllJobs(baseUrl, user, apiToken) {
|
|
18
|
+
const resp = await axios.get(`${baseUrl}/api/json?tree=jobs[name,url,color,_class]`, {
|
|
12
19
|
auth: { username: user, password: apiToken },
|
|
13
20
|
});
|
|
14
21
|
return resp.data.jobs || [];
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
async function
|
|
24
|
+
async function fetchJobParams(baseUrl, user, apiToken, jobPath) {
|
|
25
|
+
try {
|
|
26
|
+
const url = `${baseUrl}${jobPath}/api/json?tree=property[parameterDefinitions[name,type,defaultParameterValue[value],description,choices]]`;
|
|
27
|
+
const resp = await axios.get(url, { auth: { username: user, password: apiToken } });
|
|
28
|
+
const props = resp.data.property || [];
|
|
29
|
+
for (const p of props) {
|
|
30
|
+
if (p.parameterDefinitions) return p.parameterDefinitions;
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function triggerBuild({ baseUrl, user, apiToken, buildToken, jobPath, params }) {
|
|
18
39
|
const url = `${baseUrl}${jobPath}/buildWithParameters`;
|
|
19
40
|
await axios.post(url, null, {
|
|
20
|
-
params: { token: buildToken,
|
|
41
|
+
params: { token: buildToken, cause: 'MCP trigger', ...params },
|
|
21
42
|
auth: { username: user, password: apiToken },
|
|
22
43
|
});
|
|
23
44
|
}
|
|
24
45
|
|
|
25
|
-
module.exports = { validateAndFetchUser,
|
|
46
|
+
module.exports = { openJenkinsTokenPage, validateAndFetchUser, fetchAllJobs, fetchJobParams, triggerBuild };
|
package/auth/slack-auth.js
CHANGED
|
@@ -1,72 +1,31 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
1
|
const axios = require('axios');
|
|
3
|
-
const open = require('open');
|
|
4
|
-
const { saveSlackCredentials } = require('./credentials');
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const resp = await axios.post(SLACK_TOKEN_URL, null, {
|
|
26
|
-
params: { client_id: clientId, client_secret: clientSecret, code, redirect_uri: redirectUri },
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const data = resp.data;
|
|
30
|
-
if (!data.ok) throw new Error(data.error);
|
|
31
|
-
|
|
32
|
-
const creds = {
|
|
33
|
-
botToken: data.access_token,
|
|
34
|
-
userToken: data.authed_user?.access_token,
|
|
35
|
-
teamName: data.team?.name,
|
|
36
|
-
userName: data.authed_user?.id,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
saveSlackCredentials(creds);
|
|
40
|
-
res.send('✅ Slack connected! You can close this window.');
|
|
41
|
-
server.close();
|
|
42
|
-
resolve(creds);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
res.send(`❌ Error: ${err.message}`);
|
|
45
|
-
server.close();
|
|
46
|
-
reject(err);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const server = app.listen(port, () => {
|
|
51
|
-
console.error(`Opening Slack OAuth... (listening on port ${port})`);
|
|
52
|
-
open(authUrl);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
setTimeout(() => { server.close(); reject(new Error('OAuth timeout')); }, 120000);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function getSlackUserInfo(token) {
|
|
60
|
-
const resp = await axios.get('https://slack.com/api/auth.test', {
|
|
61
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
62
|
-
});
|
|
63
|
-
return resp.data;
|
|
3
|
+
async function sendSlackDM(botToken, userId, message) {
|
|
4
|
+
// Open DM channel with user
|
|
5
|
+
const openResp = await axios.post('https://slack.com/api/conversations.open',
|
|
6
|
+
{ users: userId },
|
|
7
|
+
{ headers: { Authorization: `Bearer ${botToken}`, 'Content-Type': 'application/json' } }
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
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
|
|
15
|
+
const msgResp = await axios.post('https://slack.com/api/chat.postMessage',
|
|
16
|
+
{ channel: channelId, text: message },
|
|
17
|
+
{ headers: { Authorization: `Bearer ${botToken}`, 'Content-Type': 'application/json' } }
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!msgResp.data.ok) throw new Error(`Slack message failed: ${msgResp.data.error}`);
|
|
64
21
|
}
|
|
65
22
|
|
|
66
|
-
async function
|
|
67
|
-
await axios.post('https://slack.com/api/chat.postMessage',
|
|
68
|
-
|
|
69
|
-
|
|
23
|
+
async function postToChannel(botToken, channel, message) {
|
|
24
|
+
const resp = await axios.post('https://slack.com/api/chat.postMessage',
|
|
25
|
+
{ channel, text: message },
|
|
26
|
+
{ headers: { Authorization: `Bearer ${botToken}`, 'Content-Type': 'application/json' } }
|
|
27
|
+
);
|
|
28
|
+
if (!resp.data.ok) throw new Error(`Slack post failed: ${resp.data.error}`);
|
|
70
29
|
}
|
|
71
30
|
|
|
72
|
-
module.exports = {
|
|
31
|
+
module.exports = { sendSlackDM, postToChannel };
|
package/mcp-server.js
CHANGED
|
@@ -2,38 +2,38 @@
|
|
|
2
2
|
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
3
3
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
4
4
|
const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
5
|
-
const { getCredentials, saveJenkinsCredentials, saveJobs, getJobs,
|
|
6
|
-
const { validateAndFetchUser, triggerBuild } = require('./auth/jenkins-auth');
|
|
7
|
-
const {
|
|
5
|
+
const { getCredentials, saveJenkinsCredentials, saveSlackConfig, saveJobs, getJobs, clearAll } = require('./auth/credentials');
|
|
6
|
+
const { openJenkinsTokenPage, validateAndFetchUser, fetchAllJobs, fetchJobParams, triggerBuild } = require('./auth/jenkins-auth');
|
|
7
|
+
const { 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: '1.0.4' }, { capabilities: { tools: {} } });
|
|
10
10
|
|
|
11
11
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
12
12
|
tools: [
|
|
13
13
|
{
|
|
14
14
|
name: 'login_jenkins',
|
|
15
|
-
description: 'Login to Jenkins. Validates credentials and
|
|
15
|
+
description: 'Login to Jenkins. Opens browser to Jenkins token page for new users. Validates credentials and auto-discovers all jobs.',
|
|
16
16
|
inputSchema: {
|
|
17
17
|
type: 'object',
|
|
18
18
|
properties: {
|
|
19
|
-
baseUrl: { type: 'string', description: 'Jenkins base URL (e.g. https://
|
|
19
|
+
baseUrl: { type: 'string', description: 'Jenkins base URL (e.g. https://jenkins.example.com)' },
|
|
20
20
|
user: { type: 'string', description: 'Jenkins username' },
|
|
21
|
-
apiToken: { type: 'string', description: 'Jenkins API token' },
|
|
22
|
-
buildToken: { type: 'string', description: 'Remote build trigger token
|
|
21
|
+
apiToken: { type: 'string', description: 'Jenkins API token (get from Jenkins → User → Configure → API Token)' },
|
|
22
|
+
buildToken: { type: 'string', description: 'Remote build trigger token' },
|
|
23
23
|
},
|
|
24
|
-
required: ['baseUrl'
|
|
24
|
+
required: ['baseUrl'],
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
name: '
|
|
29
|
-
description: '
|
|
28
|
+
name: 'setup_slack',
|
|
29
|
+
description: 'Configure Slack notifications. Set bot token and your Slack User ID to receive DMs on build trigger.',
|
|
30
30
|
inputSchema: {
|
|
31
31
|
type: 'object',
|
|
32
32
|
properties: {
|
|
33
|
-
|
|
34
|
-
|
|
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
35
|
},
|
|
36
|
-
required: ['
|
|
36
|
+
required: ['botToken', 'userId'],
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -42,37 +42,44 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
42
42
|
inputSchema: { type: 'object', properties: {} },
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
|
-
name: '
|
|
46
|
-
description: '
|
|
45
|
+
name: 'list_jobs',
|
|
46
|
+
description: 'List all available Jenkins jobs in table format with status. Supports filtering by name.',
|
|
47
47
|
inputSchema: {
|
|
48
48
|
type: 'object',
|
|
49
49
|
properties: {
|
|
50
|
-
|
|
51
|
-
jobPath: { type: 'string', description: 'Jenkins job path (e.g. /view/track-and-trace/job/track-and-trace)' },
|
|
52
|
-
name: { type: 'string', description: 'Friendly job name' },
|
|
53
|
-
defaultBranch: { type: 'string', description: 'Default branch if none specified' },
|
|
50
|
+
filter: { type: 'string', description: 'Filter jobs by name (e.g. "tms", "optima")' },
|
|
54
51
|
},
|
|
55
|
-
required: ['command', 'jobPath', 'name'],
|
|
56
52
|
},
|
|
57
53
|
},
|
|
58
54
|
{
|
|
59
|
-
name: '
|
|
60
|
-
description: '
|
|
61
|
-
inputSchema: {
|
|
55
|
+
name: 'job_details',
|
|
56
|
+
description: 'Get build parameters for a specific Jenkins job in table format.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
jobName: { type: 'string', description: 'Jenkins job name (e.g. tms-docker-build-new)' },
|
|
61
|
+
},
|
|
62
|
+
required: ['jobName'],
|
|
63
|
+
},
|
|
62
64
|
},
|
|
63
65
|
{
|
|
64
66
|
name: 'trigger_build',
|
|
65
|
-
description: 'Trigger a Jenkins build
|
|
67
|
+
description: 'Trigger a Jenkins build with parameters. Sends Slack DM if configured.',
|
|
66
68
|
inputSchema: {
|
|
67
69
|
type: 'object',
|
|
68
70
|
properties: {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
jobName: { type: 'string', description: 'Jenkins job name' },
|
|
72
|
+
params: { type: 'object', description: 'Build parameters as key-value pairs (e.g. {"BRANCH": "main"})' },
|
|
73
|
+
notifyChannel: { type: 'string', description: 'Slack channel to notify (optional, e.g. #deployments)' },
|
|
72
74
|
},
|
|
73
|
-
required: ['
|
|
75
|
+
required: ['jobName'],
|
|
74
76
|
},
|
|
75
77
|
},
|
|
78
|
+
{
|
|
79
|
+
name: 'refresh_jobs',
|
|
80
|
+
description: 'Re-fetch all jobs from Jenkins (use after new jobs are created)',
|
|
81
|
+
inputSchema: { type: 'object', properties: {} },
|
|
82
|
+
},
|
|
76
83
|
{
|
|
77
84
|
name: 'whoami',
|
|
78
85
|
description: 'Get current Jenkins and Slack user details',
|
|
@@ -80,7 +87,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
80
87
|
},
|
|
81
88
|
{
|
|
82
89
|
name: 'logout',
|
|
83
|
-
description: 'Clear all stored credentials',
|
|
90
|
+
description: 'Clear all stored credentials and jobs',
|
|
84
91
|
inputSchema: { type: 'object', properties: {} },
|
|
85
92
|
},
|
|
86
93
|
],
|
|
@@ -92,94 +99,214 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
92
99
|
try {
|
|
93
100
|
switch (name) {
|
|
94
101
|
case 'login_jenkins': {
|
|
102
|
+
// If only baseUrl provided, open browser for user to get token
|
|
103
|
+
if (!args.user || !args.apiToken) {
|
|
104
|
+
const tokenUrl = await openJenkinsTokenPage(args.baseUrl);
|
|
105
|
+
return text(
|
|
106
|
+
`🌐 Opening Jenkins in your browser...\n\n` +
|
|
107
|
+
`**Steps to get your credentials:**\n` +
|
|
108
|
+
`1. Login at: ${args.baseUrl}\n` +
|
|
109
|
+
`2. Go to: ${tokenUrl}\n` +
|
|
110
|
+
`3. Under "API Token" → Click "Add new Token" → Generate\n` +
|
|
111
|
+
`4. Copy the token\n` +
|
|
112
|
+
`5. For build trigger token: Go to Job → Configure → Build Triggers → "Trigger builds remotely" → Copy token\n\n` +
|
|
113
|
+
`Then call login_jenkins again with all 4 parameters:\n` +
|
|
114
|
+
`- baseUrl: ${args.baseUrl}\n` +
|
|
115
|
+
`- user: your_username\n` +
|
|
116
|
+
`- apiToken: (the token you just generated)\n` +
|
|
117
|
+
`- buildToken: (the remote trigger token)`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validate credentials
|
|
95
122
|
const userInfo = await validateAndFetchUser(args.baseUrl, args.user, args.apiToken);
|
|
96
123
|
saveJenkinsCredentials(args);
|
|
97
|
-
|
|
124
|
+
|
|
125
|
+
// Auto-discover all jobs
|
|
126
|
+
const allJobs = await fetchAllJobs(args.baseUrl, args.user, args.apiToken);
|
|
127
|
+
const buildableJobs = {};
|
|
128
|
+
for (const j of allJobs) {
|
|
129
|
+
if (j._class === 'com.cloudbees.hudson.plugins.folder.Folder') continue;
|
|
130
|
+
buildableJobs[j.name] = { path: `/job/${encodeURIComponent(j.name)}`, name: j.name, color: j.color || 'unknown' };
|
|
131
|
+
}
|
|
132
|
+
saveJobs(buildableJobs);
|
|
133
|
+
|
|
134
|
+
const jobCount = Object.keys(buildableJobs).length;
|
|
135
|
+
let msg = `✅ Jenkins login successful! Welcome, ${userInfo.fullName}\n`;
|
|
136
|
+
msg += `📋 Auto-discovered ${jobCount} jobs.\n\n`;
|
|
137
|
+
msg += `Use **list_jobs** to see all jobs or **list_jobs filter="tms"** to search.\n`;
|
|
138
|
+
msg += `Use **job_details** to see parameters before triggering.`;
|
|
139
|
+
|
|
140
|
+
const creds = getCredentials();
|
|
141
|
+
if (!creds.slack) {
|
|
142
|
+
msg += `\n\n💡 Set up Slack notifications with **setup_slack** to get DMs on build trigger.`;
|
|
143
|
+
}
|
|
144
|
+
return text(msg);
|
|
98
145
|
}
|
|
99
146
|
|
|
100
|
-
case '
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
case 'setup_slack': {
|
|
148
|
+
saveSlackConfig({ botToken: args.botToken, userId: args.userId });
|
|
149
|
+
|
|
150
|
+
// Test by sending a welcome DM
|
|
151
|
+
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.`);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return text(`⚠️ Slack config saved but test DM failed: ${err.message}\nCheck your bot token and user ID.`);
|
|
156
|
+
}
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
case 'status': {
|
|
113
160
|
const creds = getCredentials();
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
161
|
+
const jobCount = Object.keys(getJobs()).length;
|
|
162
|
+
const jenkins = creds.jenkins
|
|
163
|
+
? `✅ Jenkins: ${creds.jenkins.user} @ ${creds.jenkins.baseUrl}`
|
|
164
|
+
: '❌ Jenkins: not logged in. Use **login_jenkins** to connect.';
|
|
165
|
+
const slack = creds.slack
|
|
166
|
+
? `✅ Slack: User ID ${creds.slack.userId} (DM notifications enabled)`
|
|
167
|
+
: '❌ Slack: not configured. Use **setup_slack** to enable notifications.';
|
|
168
|
+
return text(`${jenkins}\n${slack}\n📋 Jobs available: ${jobCount}`);
|
|
117
169
|
}
|
|
118
170
|
|
|
119
|
-
case '
|
|
171
|
+
case 'list_jobs': {
|
|
172
|
+
const creds = getCredentials();
|
|
173
|
+
if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use **login_jenkins** first.');
|
|
174
|
+
|
|
120
175
|
const jobs = getJobs();
|
|
121
|
-
jobs
|
|
122
|
-
|
|
123
|
-
|
|
176
|
+
if (!Object.keys(jobs).length) return text('No jobs found. Use **refresh_jobs** to re-fetch.');
|
|
177
|
+
|
|
178
|
+
let filtered = Object.entries(jobs);
|
|
179
|
+
if (args.filter) {
|
|
180
|
+
const f = args.filter.toLowerCase();
|
|
181
|
+
filtered = filtered.filter(([n]) => n.toLowerCase().includes(f));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!filtered.length) return text(`No jobs matching "${args.filter}".`);
|
|
185
|
+
|
|
186
|
+
let table = `| # | Job Name | Status |\n|---|----------|--------|\n`;
|
|
187
|
+
filtered.slice(0, 50).forEach(([, job], i) => {
|
|
188
|
+
const status = job.color === 'blue' ? '✅ Success' : job.color === 'red' ? '❌ Failed' : job.color === 'disabled' ? '⏸️ Disabled' : job.color === 'notbuilt' ? '⬜ Not built' : `⚠️ ${job.color}`;
|
|
189
|
+
table += `| ${i + 1} | ${job.name} | ${status} |\n`;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (filtered.length > 50) table += `\n_...${filtered.length - 50} more. Narrow with filter._`;
|
|
193
|
+
table += `\n\nUse **job_details jobName="<name>"** to see parameters.`;
|
|
194
|
+
return text(table);
|
|
124
195
|
}
|
|
125
196
|
|
|
126
|
-
case '
|
|
197
|
+
case 'job_details': {
|
|
198
|
+
const creds = getCredentials();
|
|
199
|
+
if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use **login_jenkins** first.');
|
|
200
|
+
|
|
127
201
|
const jobs = getJobs();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
202
|
+
const job = jobs[args.jobName];
|
|
203
|
+
if (!job) return text(`❌ Job "${args.jobName}" not found. Use **list_jobs** to see available jobs.`);
|
|
204
|
+
|
|
205
|
+
const params = await fetchJobParams(creds.jenkins.baseUrl, creds.jenkins.user, creds.jenkins.apiToken, job.path);
|
|
206
|
+
|
|
207
|
+
let msg = `## ${job.name}\n`;
|
|
208
|
+
msg += `**Status:** ${job.color === 'blue' ? '✅ Success' : job.color === 'red' ? '❌ Failed' : job.color}\n\n`;
|
|
209
|
+
|
|
210
|
+
if (params.length === 0) {
|
|
211
|
+
msg += `**Parameters:** None required\n`;
|
|
212
|
+
msg += `\n→ Trigger with: **trigger_build jobName="${job.name}"**`;
|
|
213
|
+
} else {
|
|
214
|
+
msg += `| Parameter | Type | Default | Choices | Description |\n|-----------|------|---------|---------|-------------|\n`;
|
|
215
|
+
params.forEach(p => {
|
|
216
|
+
const def = p.defaultParameterValue?.value || '-';
|
|
217
|
+
const type = (p.type || '').replace('ParameterDefinition', '');
|
|
218
|
+
const desc = p.description || '-';
|
|
219
|
+
const choices = p.choices ? p.choices.join(', ') : '-';
|
|
220
|
+
msg += `| ${p.name} | ${type} | ${def} | ${choices} | ${desc} |\n`;
|
|
221
|
+
});
|
|
222
|
+
const example = params.map(p => `"${p.name}": "${p.defaultParameterValue?.value || 'value'}"`).join(', ');
|
|
223
|
+
msg += `\n→ Trigger with: **trigger_build jobName="${job.name}" params={${example}}**`;
|
|
224
|
+
}
|
|
225
|
+
return text(msg);
|
|
131
226
|
}
|
|
132
227
|
|
|
133
228
|
case 'trigger_build': {
|
|
134
229
|
const creds = getCredentials();
|
|
135
|
-
if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use login_jenkins first.');
|
|
230
|
+
if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use **login_jenkins** first.');
|
|
136
231
|
|
|
137
232
|
const jobs = getJobs();
|
|
138
|
-
const job = jobs[args.
|
|
139
|
-
if (!job) return text(`❌
|
|
233
|
+
const job = jobs[args.jobName];
|
|
234
|
+
if (!job) return text(`❌ Job "${args.jobName}" not found. Use **list_jobs** to see available jobs.`);
|
|
140
235
|
|
|
141
|
-
const
|
|
236
|
+
const params = args.params || {};
|
|
142
237
|
await triggerBuild({
|
|
143
238
|
baseUrl: creds.jenkins.baseUrl,
|
|
144
239
|
user: creds.jenkins.user,
|
|
145
240
|
apiToken: creds.jenkins.apiToken,
|
|
146
241
|
buildToken: creds.jenkins.buildToken,
|
|
147
242
|
jobPath: job.path,
|
|
148
|
-
|
|
243
|
+
params,
|
|
149
244
|
});
|
|
150
245
|
|
|
151
|
-
|
|
246
|
+
const paramStr = Object.entries(params).map(([k, v]) => `${k}=${v}`).join(', ') || 'defaults';
|
|
247
|
+
let msg = `✅ **${job.name}** build triggered (${paramStr})`;
|
|
248
|
+
|
|
249
|
+
// Send Slack DM to user
|
|
250
|
+
if (creds.slack?.botToken && creds.slack?.userId) {
|
|
251
|
+
try {
|
|
252
|
+
await sendSlackDM(creds.slack.botToken, creds.slack.userId, `🚀 *${job.name}* build triggered\nParams: ${paramStr}\nTriggered by: ${creds.jenkins.user}`);
|
|
253
|
+
msg += `\n✉️ Slack DM sent to you.`;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
msg += `\n⚠️ Slack DM failed: ${err.message}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Notify channel if specified
|
|
260
|
+
if (args.notifyChannel && creds.slack?.botToken) {
|
|
261
|
+
try {
|
|
262
|
+
await postToChannel(creds.slack.botToken, args.notifyChannel, `🚀 *${job.name}* build triggered (${paramStr}) by ${creds.jenkins.user}`);
|
|
263
|
+
msg += `\n📢 Posted to ${args.notifyChannel}`;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
msg += `\n⚠️ Channel notify failed: ${err.message}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
152
268
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await postToSlack(creds.slack.botToken, args.slackChannel, `🚀 ${job.name} build triggered on branch: ${branch}`);
|
|
156
|
-
msg += `\n📢 Notified Slack channel: ${args.slackChannel}`;
|
|
269
|
+
if (!creds.slack) {
|
|
270
|
+
msg += `\n\n💡 Set up **setup_slack** to get DM notifications on every build.`;
|
|
157
271
|
}
|
|
158
272
|
|
|
159
273
|
return text(msg);
|
|
160
274
|
}
|
|
161
275
|
|
|
276
|
+
case 'refresh_jobs': {
|
|
277
|
+
const creds = getCredentials();
|
|
278
|
+
if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use **login_jenkins** first.');
|
|
279
|
+
|
|
280
|
+
const allJobs = await fetchAllJobs(creds.jenkins.baseUrl, creds.jenkins.user, creds.jenkins.apiToken);
|
|
281
|
+
const buildableJobs = {};
|
|
282
|
+
for (const j of allJobs) {
|
|
283
|
+
if (j._class === 'com.cloudbees.hudson.plugins.folder.Folder') continue;
|
|
284
|
+
buildableJobs[j.name] = { path: `/job/${encodeURIComponent(j.name)}`, name: j.name, color: j.color || 'unknown' };
|
|
285
|
+
}
|
|
286
|
+
saveJobs(buildableJobs);
|
|
287
|
+
return text(`✅ Refreshed! ${Object.keys(buildableJobs).length} jobs discovered.`);
|
|
288
|
+
}
|
|
289
|
+
|
|
162
290
|
case 'whoami': {
|
|
163
291
|
const creds = getCredentials();
|
|
164
292
|
const parts = [];
|
|
165
293
|
if (creds.jenkins) {
|
|
166
294
|
const userInfo = await validateAndFetchUser(creds.jenkins.baseUrl, creds.jenkins.user, creds.jenkins.apiToken);
|
|
167
|
-
parts.push(
|
|
295
|
+
parts.push(`**Jenkins:** ${userInfo.fullName} (${userInfo.id}) @ ${creds.jenkins.baseUrl}`);
|
|
168
296
|
} else {
|
|
169
|
-
parts.push('Jenkins
|
|
297
|
+
parts.push('**Jenkins:** not logged in');
|
|
170
298
|
}
|
|
171
|
-
if (creds.slack
|
|
172
|
-
|
|
173
|
-
parts.push(`Slack: ${slackInfo.user} @ ${slackInfo.team}`);
|
|
299
|
+
if (creds.slack) {
|
|
300
|
+
parts.push(`**Slack:** User ID ${creds.slack.userId} (DM enabled)`);
|
|
174
301
|
} else {
|
|
175
|
-
parts.push('Slack
|
|
302
|
+
parts.push('**Slack:** not configured');
|
|
176
303
|
}
|
|
177
304
|
return text(parts.join('\n'));
|
|
178
305
|
}
|
|
179
306
|
|
|
180
307
|
case 'logout': {
|
|
181
308
|
clearAll();
|
|
182
|
-
return text('✅ All credentials cleared.');
|
|
309
|
+
return text('✅ All credentials and jobs cleared.');
|
|
183
310
|
}
|
|
184
311
|
|
|
185
312
|
default:
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jenkins-slack-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server to trigger Jenkins builds from any IDE
|
|
3
|
+
"version": "2.0.0",
|
|
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": {
|
|
7
7
|
"jenkins-slack-mcp": "mcp-server.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node mcp-server.js"
|
|
11
|
-
"server": "node server.js"
|
|
10
|
+
"start": "node mcp-server.js"
|
|
12
11
|
},
|
|
13
12
|
"keywords": [
|
|
14
13
|
"mcp",
|
package/server.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const { getCredentials, getJobs } = require('./auth/credentials');
|
|
3
|
-
const { triggerBuild } = require('./auth/jenkins-auth');
|
|
4
|
-
|
|
5
|
-
const app = express();
|
|
6
|
-
app.use(express.urlencoded({ extended: true }));
|
|
7
|
-
|
|
8
|
-
app.post('/slack/command', async (req, res) => {
|
|
9
|
-
const { command, text, user_name } = req.body;
|
|
10
|
-
const branch = text.trim() || undefined;
|
|
11
|
-
const creds = getCredentials();
|
|
12
|
-
const jobs = getJobs();
|
|
13
|
-
|
|
14
|
-
if (!creds.jenkins) return res.json({ text: '❌ Jenkins not configured. Run MCP login_jenkins first.' });
|
|
15
|
-
|
|
16
|
-
const job = jobs[command];
|
|
17
|
-
if (!job) return res.json({ text: `❌ Unknown job: ${command}` });
|
|
18
|
-
|
|
19
|
-
const branchName = branch || job.defaultBranch;
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await triggerBuild({
|
|
23
|
-
baseUrl: creds.jenkins.baseUrl,
|
|
24
|
-
user: creds.jenkins.user,
|
|
25
|
-
apiToken: creds.jenkins.apiToken,
|
|
26
|
-
buildToken: creds.jenkins.buildToken,
|
|
27
|
-
jobPath: job.path,
|
|
28
|
-
branch: branchName,
|
|
29
|
-
});
|
|
30
|
-
res.json({ response_type: 'in_channel', text: `✅ *${job.name}* build triggered on \`${branchName}\` by @${user_name}` });
|
|
31
|
-
} catch (err) {
|
|
32
|
-
res.json({ response_type: 'ephemeral', text: `❌ Failed: ${err.message}` });
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
app.get('/health', (_, res) => res.send('ok'));
|
|
37
|
-
|
|
38
|
-
const PORT = process.env.PORT || 3001;
|
|
39
|
-
app.listen(PORT, () => console.log(`Slack command server on port ${PORT}`));
|