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 CHANGED
@@ -1,16 +1,18 @@
1
1
  # jenkins-slack-mcp
2
2
 
3
- MCP server to trigger Jenkins builds from any IDE (Amazon Q, VS Code, Cursor) with Slack notifications. Zero config login once and it remembers.
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 github:Seetharam1999/jenkins-slack-mcp
8
+ npm install -g jenkins-slack-mcp
9
9
  ```
10
10
 
11
11
  ## Register in your IDE
12
12
 
13
- **Amazon Q** (`~/.aws/amazonq/mcp.json`):
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
- **VS Code / Cursor** (`.vscode/mcp.json`):
28
+ ### VS Code
29
+
30
+ `.vscode/mcp.json`:
27
31
  ```json
28
32
  {
29
33
  "mcpServers": {
30
- "jenkins-slack": {
31
- "command": "jenkins-slack-mcp"
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
- **Without global install** (npx):
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": ["github:Seetharam1999/jenkins-slack-mcp"]
68
+ "args": ["-y", "jenkins-slack-mcp"]
44
69
  }
45
70
  }
46
71
  }
47
72
  ```
48
73
 
49
- ## First-time Setup (from IDE chat)
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
- ### 1. Login to Jenkins
119
+ Then login with full credentials:
52
120
  ```
53
121
  login_jenkins:
54
- baseUrl: https://your-jenkins.com
122
+ baseUrl: https://jenkins.example.com
55
123
  user: your_username
56
124
  apiToken: your_api_token
57
- buildToken: your_remote_trigger_token
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
- ### 2. Login to Slack
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
- login_slack:
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
- ### 3. Register Jobs
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
- add_job:
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
- ### 4. Trigger Builds
160
+ ### Step 5 (Optional): Setup Slack DMs
161
+
78
162
  ```
79
- trigger_build:
80
- job: /buildtt
81
- branch: feature/my-branch
82
- slackChannel: #deployments
163
+ setup_slack:
164
+ botToken: xoxb-your-bot-token
165
+ userId: U0123456789
83
166
  ```
84
167
 
85
- ## Available Tools
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
- | Tool | Description |
88
- |------|-------------|
89
- | `login_jenkins` | Authenticate with Jenkins |
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
- ## Slack Slash Commands (bonus)
177
+ After setup, every `trigger_build` sends you a DM automatically.
99
178
 
100
- If you also want `/buildtt main` from Slack directly:
179
+ ---
101
180
 
102
- ```bash
103
- node server.js
104
- # Expose with ngrok:
105
- ngrok http 3001
106
- ```
181
+ ## Available Tools
107
182
 
108
- Set Slack app slash command URL to `https://your-ngrok-url/slack/command`
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
 
@@ -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 saveSlackCredentials({ botToken, userToken, teamName, userName }) {
34
+ function saveSlackConfig({ botToken, userId }) {
38
35
  const config = loadConfig();
39
- config.slack = { botToken, userToken, teamName, userName };
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, saveSlackCredentials, saveJobs, getJobs, isConfigured, clearAll };
55
+ module.exports = { getCredentials, saveJenkinsCredentials, saveSlackConfig, saveJobs, getJobs, clearAll };
@@ -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 fetchJobs(baseUrl, user, apiToken) {
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 triggerBuild({ baseUrl, user, apiToken, buildToken, jobPath, branch }) {
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, BRANCH: branch, cause: `MCP trigger for ${branch}` },
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, fetchJobs, triggerBuild };
46
+ module.exports = { openJenkinsTokenPage, validateAndFetchUser, fetchAllJobs, fetchJobParams, triggerBuild };
@@ -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
- const SLACK_OAUTH_URL = 'https://slack.com/oauth/v2/authorize';
7
- const SLACK_TOKEN_URL = 'https://slack.com/api/oauth.v2.access';
8
-
9
- async function slackOAuthFlow({ clientId, clientSecret, scopes, port = 9876 }) {
10
- return new Promise((resolve, reject) => {
11
- const app = express();
12
- const redirectUri = `http://localhost:${port}/slack/callback`;
13
-
14
- const authUrl = `${SLACK_OAUTH_URL}?client_id=${clientId}&scope=${scopes.bot.join(',')}&user_scope=${scopes.user.join(',')}&redirect_uri=${encodeURIComponent(redirectUri)}`;
15
-
16
- app.get('/slack/callback', async (req, res) => {
17
- const { code, error } = req.query;
18
- if (error) {
19
- res.send('❌ Slack auth denied');
20
- server.close();
21
- return reject(new Error(error));
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 postToSlack(token, channel, text) {
67
- await axios.post('https://slack.com/api/chat.postMessage', { channel, text }, {
68
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
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 = { slackOAuthFlow, getSlackUserInfo, postToSlack };
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, isConfigured, clearAll } = require('./auth/credentials');
6
- const { validateAndFetchUser, triggerBuild } = require('./auth/jenkins-auth');
7
- const { slackOAuthFlow, getSlackUserInfo, postToSlack } = require('./auth/slack-auth');
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.0' }, { capabilities: { tools: {} } });
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 stores them securely in ~/.jenkins-slack-mcp/',
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://ci-ind.gopando.in)' },
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 configured in Jenkins job' },
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', 'user', 'apiToken', 'buildToken'],
24
+ required: ['baseUrl'],
25
25
  },
26
26
  },
27
27
  {
28
- name: 'login_slack',
29
- description: 'Login to Slack via OAuth. Opens browser for authentication.',
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
- clientId: { type: 'string', description: 'Slack App Client ID' },
34
- clientSecret: { type: 'string', description: 'Slack App Client Secret' },
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: ['clientId', 'clientSecret'],
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: 'add_job',
46
- description: 'Register a Jenkins job that can be triggered',
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
- command: { type: 'string', description: 'Trigger command name (e.g. /buildtt)' },
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: 'list_jobs',
60
- description: 'List all registered Jenkins jobs',
61
- inputSchema: { type: 'object', properties: {} },
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 and optionally notify Slack',
67
+ description: 'Trigger a Jenkins build with parameters. Sends Slack DM if configured.',
66
68
  inputSchema: {
67
69
  type: 'object',
68
70
  properties: {
69
- job: { type: 'string', description: 'Job command (e.g. /buildtt) or job name' },
70
- branch: { type: 'string', description: 'Branch to build' },
71
- slackChannel: { type: 'string', description: 'Slack channel to notify (optional)' },
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: ['job'],
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
- return text(`✅ Jenkins login successful! Welcome, ${userInfo.fullName} (${userInfo.id})`);
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 'login_slack': {
101
- const creds = await slackOAuthFlow({
102
- clientId: args.clientId,
103
- clientSecret: args.clientSecret,
104
- scopes: {
105
- bot: ['commands', 'chat:write', 'channels:read'],
106
- user: ['chat:write'],
107
- },
108
- });
109
- return text(`✅ Slack connected! Team: ${creds.teamName}`);
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 jenkins = creds.jenkins ? `✅ Jenkins: ${creds.jenkins.user} @ ${creds.jenkins.baseUrl}` : '❌ Jenkins: not logged in';
115
- const slack = creds.slack ? `✅ Slack: ${creds.slack.teamName}` : '❌ Slack: not logged in';
116
- return text(`${jenkins}\n${slack}`);
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 'add_job': {
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[args.command] = { path: args.jobPath, name: args.name, defaultBranch: args.defaultBranch || 'main' };
122
- saveJobs(jobs);
123
- return text(`✅ Job registered: ${args.command} → ${args.name} (default: ${args.defaultBranch || 'main'})`);
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 'list_jobs': {
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
- if (!Object.keys(jobs).length) return text('No jobs configured. Use add_job to register one.');
129
- const list = Object.entries(jobs).map(([cmd, j]) => `• ${cmd} ${j.name} (default: ${j.defaultBranch})`).join('\n');
130
- return text(list);
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.job];
139
- if (!job) return text(`❌ Unknown job: ${args.job}. Available: ${Object.keys(jobs).join(', ')}`);
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 branch = args.branch || job.defaultBranch;
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
- branch,
243
+ params,
149
244
  });
150
245
 
151
- let msg = `✅ *${job.name}* build triggered on \`${branch}\``;
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
- // Notify Slack if channel provided and logged in
154
- if (args.slackChannel && creds.slack?.botToken) {
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(`Jenkins: ${userInfo.fullName} (${userInfo.id}) @ ${creds.jenkins.baseUrl}`);
295
+ parts.push(`**Jenkins:** ${userInfo.fullName} (${userInfo.id}) @ ${creds.jenkins.baseUrl}`);
168
296
  } else {
169
- parts.push('Jenkins: not logged in');
297
+ parts.push('**Jenkins:** not logged in');
170
298
  }
171
- if (creds.slack?.botToken) {
172
- const slackInfo = await getSlackUserInfo(creds.slack.botToken);
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: not logged in');
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": "1.0.1",
4
- "description": "MCP server to trigger Jenkins builds from any IDE with Slack notifications. Auto-configures via interactive login.",
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}`));