jenkins-slack-mcp 1.0.3 → 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,6 +1,6 @@
1
1
  # jenkins-slack-mcp
2
2
 
3
- MCP server to trigger Jenkins builds from any IDE (Amazon Q, VS Code, Cursor, Claude Desktop) 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
 
@@ -10,10 +10,9 @@ npm install -g jenkins-slack-mcp
10
10
 
11
11
  ## Register in your IDE
12
12
 
13
- ### Amazon Q (VS Code / JetBrains)
14
-
15
- Edit `~/.aws/amazonq/mcp.json`:
13
+ ### Amazon Q
16
14
 
15
+ `~/.aws/amazonq/mcp.json`:
17
16
  ```json
18
17
  {
19
18
  "mcpServers": {
@@ -26,51 +25,40 @@ Edit `~/.aws/amazonq/mcp.json`:
26
25
  }
27
26
  ```
28
27
 
29
- ### VS Code (Copilot / Cline / Roo)
30
-
31
- Edit `.vscode/mcp.json` in your workspace:
28
+ ### VS Code
32
29
 
30
+ `.vscode/mcp.json`:
33
31
  ```json
34
32
  {
35
33
  "mcpServers": {
36
- "jenkins-slack": {
37
- "command": "jenkins-slack-mcp"
38
- }
34
+ "jenkins-slack": { "command": "jenkins-slack-mcp" }
39
35
  }
40
36
  }
41
37
  ```
42
38
 
43
39
  ### Claude Desktop
44
40
 
45
- Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
46
-
41
+ `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
47
42
  ```json
48
43
  {
49
44
  "mcpServers": {
50
- "jenkins-slack": {
51
- "command": "jenkins-slack-mcp"
52
- }
45
+ "jenkins-slack": { "command": "jenkins-slack-mcp" }
53
46
  }
54
47
  }
55
48
  ```
56
49
 
57
50
  ### Cursor
58
51
 
59
- Edit `.cursor/mcp.json` in your workspace:
60
-
52
+ `.cursor/mcp.json`:
61
53
  ```json
62
54
  {
63
55
  "mcpServers": {
64
- "jenkins-slack": {
65
- "command": "jenkins-slack-mcp"
66
- }
56
+ "jenkins-slack": { "command": "jenkins-slack-mcp" }
67
57
  }
68
58
  }
69
59
  ```
70
60
 
71
- ### Without global install (npx)
72
-
73
- Use this in any of the above configs instead:
61
+ ### Without global install
74
62
 
75
63
  ```json
76
64
  {
@@ -83,72 +71,130 @@ Use this in any of the above configs instead:
83
71
  }
84
72
  ```
85
73
 
86
- ## 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
87
112
 
88
- Once the MCP is registered, use these tools from your IDE chat:
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.
89
118
 
90
- ### 1. Login to Jenkins
119
+ Then login with full credentials:
91
120
  ```
92
121
  login_jenkins:
93
122
  baseUrl: https://jenkins.example.com
94
123
  user: your_username
95
124
  apiToken: your_api_token
96
- buildToken: your_remote_trigger_token
125
+ buildToken: your_build_trigger_token
97
126
  ```
127
+ → All jobs auto-discovered!
128
+
129
+ ### Step 2: Browse Jobs
98
130
 
99
- ### 2. Login to Slack (optional, for notifications)
100
131
  ```
101
- login_slack:
102
- clientId: your_slack_app_client_id
103
- clientSecret: your_slack_app_client_secret
132
+ list_jobs # all jobs
133
+ list_jobs filter="tms" # filter by name
104
134
  ```
105
- Opens browser → OAuth → done.
106
135
 
107
- ### 3. Register Jobs
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
+
108
145
  ```
109
- add_job:
110
- command: /build-backend
111
- jobPath: /job/my-backend-service
112
- name: Backend Service
113
- defaultBranch: main
146
+ job_details jobName="tms-docker-build-new"
114
147
  ```
115
148
 
116
- ### 4. Trigger Builds
149
+ Output:
150
+ | Parameter | Type | Default | Choices | Description |
151
+ |-----------|------|---------|---------|-------------|
152
+ | BRANCH | String | main | - | Branch to build |
153
+
154
+ ### Step 4: Trigger Build
155
+
117
156
  ```
118
- trigger_build:
119
- job: /build-backend
120
- branch: feature/my-branch
121
- slackChannel: #deployments
157
+ trigger_build jobName="tms-docker-build-new" params={"BRANCH": "feature/xyz"}
122
158
  ```
123
159
 
124
- ## Available Tools
160
+ ### Step 5 (Optional): Setup Slack DMs
125
161
 
126
- | Tool | Description |
127
- |------|-------------|
128
- | `login_jenkins` | Authenticate with Jenkins |
129
- | `login_slack` | OAuth login to Slack (opens browser) |
130
- | `status` | Check login status |
131
- | `whoami` | Get user details from both services |
132
- | `add_job` | Register a Jenkins job |
133
- | `list_jobs` | Show registered jobs |
134
- | `trigger_build` | Build + optional Slack notify |
135
- | `logout` | Clear stored credentials |
162
+ ```
163
+ setup_slack:
164
+ botToken: xoxb-your-bot-token
165
+ userId: U0123456789
166
+ ```
136
167
 
137
- ## Credentials
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"
138
172
 
139
- Stored at `~/.jenkins-slack-mcp/config.json` (600 permissions, owner-only). Shared across all IDEs.
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-`)
140
176
 
141
- ## Slack Slash Commands (bonus)
177
+ After setup, every `trigger_build` sends you a DM automatically.
142
178
 
143
- If you also want `/build-app main` from Slack directly:
179
+ ---
144
180
 
145
- ```bash
146
- node server.js
147
- # Expose with ngrok:
148
- ngrok http 3001
149
- ```
181
+ ## Available Tools
182
+
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 |
194
+
195
+ ## Credentials
150
196
 
151
- Set Slack app slash command URL to `https://your-ngrok-url/slack/command`
197
+ Stored at `~/.jenkins-slack-mcp/config.json` (600 permissions, owner-only).
152
198
 
153
199
  ## Author
154
200
 
@@ -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
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. /build-app)' },
51
- jobPath: { type: 'string', description: 'Jenkins job path (e.g. /job/my-app or /view/my-view/job/my-app)' },
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. /build-app) 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,109 +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. Use login_jenkins to connect.';
115
- const slack = creds.slack ? `✅ Slack: ${creds.slack.teamName}` : '❌ Slack: not logged in. Use login_slack to connect.';
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': {
120
172
  const creds = getCredentials();
121
- if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use login_jenkins first.');
173
+ if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use **login_jenkins** first.');
122
174
 
123
175
  const jobs = getJobs();
124
- jobs[args.command] = { path: args.jobPath, name: args.name, defaultBranch: args.defaultBranch || 'main' };
125
- saveJobs(jobs);
176
+ if (!Object.keys(jobs).length) return text('No jobs found. Use **refresh_jobs** to re-fetch.');
126
177
 
127
- let msg = `✅ Job registered: ${args.command} → ${args.name} (default: ${args.defaultBranch || 'main'})`;
128
- if (!creds.slack) {
129
- msg += `\n\n💡 Tip: Connect Slack with login_slack to get build notifications in your channels.`;
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));
130
182
  }
131
- return text(msg);
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);
132
195
  }
133
196
 
134
- 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
+
135
201
  const jobs = getJobs();
136
- if (!Object.keys(jobs).length) return text('No jobs configured. Use add_job to register one.');
137
- const list = Object.entries(jobs).map(([cmd, j]) => `• ${cmd} ${j.name} (default: ${j.defaultBranch})`).join('\n');
138
- 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);
139
226
  }
140
227
 
141
228
  case 'trigger_build': {
142
229
  const creds = getCredentials();
143
- 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.');
144
231
 
145
232
  const jobs = getJobs();
146
- const job = jobs[args.job];
147
- 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.`);
148
235
 
149
- const branch = args.branch || job.defaultBranch;
236
+ const params = args.params || {};
150
237
  await triggerBuild({
151
238
  baseUrl: creds.jenkins.baseUrl,
152
239
  user: creds.jenkins.user,
153
240
  apiToken: creds.jenkins.apiToken,
154
241
  buildToken: creds.jenkins.buildToken,
155
242
  jobPath: job.path,
156
- branch,
243
+ params,
157
244
  });
158
245
 
159
- 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
+ }
160
258
 
161
- // Notify Slack if channel provided and logged in
162
- if (args.slackChannel && creds.slack?.botToken) {
163
- await postToSlack(creds.slack.botToken, args.slackChannel, `🚀 ${job.name} build triggered on branch: ${branch}`);
164
- msg += `\n📢 Notified Slack channel: ${args.slackChannel}`;
165
- } else if (args.slackChannel && !creds.slack) {
166
- msg += `\n⚠️ Slack not connected. Use login_slack to enable Slack notifications.`;
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
+ }
167
267
  }
168
268
 
169
- // Remind to connect Slack if not logged in
170
269
  if (!creds.slack) {
171
- msg += `\n\n💡 Tip: Connect Slack with login_slack to get build notifications in your channels.`;
270
+ msg += `\n\n💡 Set up **setup_slack** to get DM notifications on every build.`;
172
271
  }
173
272
 
174
273
  return text(msg);
175
274
  }
176
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
+
177
290
  case 'whoami': {
178
291
  const creds = getCredentials();
179
292
  const parts = [];
180
293
  if (creds.jenkins) {
181
294
  const userInfo = await validateAndFetchUser(creds.jenkins.baseUrl, creds.jenkins.user, creds.jenkins.apiToken);
182
- parts.push(`Jenkins: ${userInfo.fullName} (${userInfo.id}) @ ${creds.jenkins.baseUrl}`);
295
+ parts.push(`**Jenkins:** ${userInfo.fullName} (${userInfo.id}) @ ${creds.jenkins.baseUrl}`);
183
296
  } else {
184
- parts.push('Jenkins: not logged in');
297
+ parts.push('**Jenkins:** not logged in');
185
298
  }
186
- if (creds.slack?.botToken) {
187
- const slackInfo = await getSlackUserInfo(creds.slack.botToken);
188
- parts.push(`Slack: ${slackInfo.user} @ ${slackInfo.team}`);
299
+ if (creds.slack) {
300
+ parts.push(`**Slack:** User ID ${creds.slack.userId} (DM enabled)`);
189
301
  } else {
190
- parts.push('Slack: not logged in');
302
+ parts.push('**Slack:** not configured');
191
303
  }
192
304
  return text(parts.join('\n'));
193
305
  }
194
306
 
195
307
  case 'logout': {
196
308
  clearAll();
197
- return text('✅ All credentials cleared.');
309
+ return text('✅ All credentials and jobs cleared.');
198
310
  }
199
311
 
200
312
  default:
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "jenkins-slack-mcp",
3
- "version": "1.0.3",
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}`));