jenkins-slack-mcp 1.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/.gitnexus/lbug +0 -0
- package/.gitnexus/meta.json +13 -0
- package/README.md +116 -0
- package/auth/credentials.js +63 -0
- package/auth/jenkins-auth.js +25 -0
- package/auth/slack-auth.js +72 -0
- package/mcp-server.js +203 -0
- package/package.json +36 -0
- package/server.js +39 -0
package/.gitnexus/lbug
ADDED
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repoPath": "/Users/seetharam/Documents/PANDO/slack-jenkins-trigger",
|
|
3
|
+
"lastCommit": "04fe0edbd4d1e1a4afbae2cd1a54eea2ae306197",
|
|
4
|
+
"indexedAt": "2026-06-24T14:41:59.735Z",
|
|
5
|
+
"stats": {
|
|
6
|
+
"files": 7,
|
|
7
|
+
"nodes": 35,
|
|
8
|
+
"edges": 75,
|
|
9
|
+
"communities": 11,
|
|
10
|
+
"processes": 5,
|
|
11
|
+
"embeddings": 0
|
|
12
|
+
}
|
|
13
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# jenkins-slack-mcp
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g github:Seetharam1999/jenkins-slack-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Register in your IDE
|
|
12
|
+
|
|
13
|
+
**Amazon Q** (`~/.aws/amazonq/mcp.json`):
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"jenkins-slack": {
|
|
18
|
+
"command": "jenkins-slack-mcp",
|
|
19
|
+
"args": [],
|
|
20
|
+
"disabled": false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**VS Code / Cursor** (`.vscode/mcp.json`):
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"jenkins-slack": {
|
|
31
|
+
"command": "jenkins-slack-mcp"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Without global install** (npx):
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"jenkins-slack": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["github:Seetharam1999/jenkins-slack-mcp"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## First-time Setup (from IDE chat)
|
|
50
|
+
|
|
51
|
+
### 1. Login to Jenkins
|
|
52
|
+
```
|
|
53
|
+
login_jenkins:
|
|
54
|
+
baseUrl: https://your-jenkins.com
|
|
55
|
+
user: your_username
|
|
56
|
+
apiToken: your_api_token
|
|
57
|
+
buildToken: your_remote_trigger_token
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Login to Slack
|
|
61
|
+
```
|
|
62
|
+
login_slack:
|
|
63
|
+
clientId: your_slack_app_client_id
|
|
64
|
+
clientSecret: your_slack_app_client_secret
|
|
65
|
+
```
|
|
66
|
+
Opens browser → OAuth → done.
|
|
67
|
+
|
|
68
|
+
### 3. Register Jobs
|
|
69
|
+
```
|
|
70
|
+
add_job:
|
|
71
|
+
command: /buildtt
|
|
72
|
+
jobPath: /view/track-and-trace/job/track-and-trace
|
|
73
|
+
name: Track & Trace
|
|
74
|
+
defaultBranch: main
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. Trigger Builds
|
|
78
|
+
```
|
|
79
|
+
trigger_build:
|
|
80
|
+
job: /buildtt
|
|
81
|
+
branch: feature/my-branch
|
|
82
|
+
slackChannel: #deployments
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Available Tools
|
|
86
|
+
|
|
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 |
|
|
97
|
+
|
|
98
|
+
## Slack Slash Commands (bonus)
|
|
99
|
+
|
|
100
|
+
If you also want `/buildtt main` from Slack directly:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
node server.js
|
|
104
|
+
# Expose with ngrok:
|
|
105
|
+
ngrok http 3001
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Set Slack app slash command URL to `https://your-ngrok-url/slack/command`
|
|
109
|
+
|
|
110
|
+
## Credentials
|
|
111
|
+
|
|
112
|
+
Stored at `~/.jenkins-slack-mcp/config.json` (600 permissions, owner-only).
|
|
113
|
+
|
|
114
|
+
## Author
|
|
115
|
+
|
|
116
|
+
[Seetharam1999](https://github.com/Seetharam1999)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.jenkins-slack-mcp');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
function ensureConfigDir() {
|
|
9
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { mode: 0o700 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
ensureConfigDir();
|
|
14
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
15
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function saveConfig(config) {
|
|
19
|
+
ensureConfigDir();
|
|
20
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCredentials() {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
return {
|
|
26
|
+
jenkins: config.jenkins || null,
|
|
27
|
+
slack: config.slack || null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveJenkinsCredentials({ baseUrl, user, apiToken, buildToken }) {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
config.jenkins = { baseUrl, user, apiToken, buildToken };
|
|
34
|
+
saveConfig(config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveSlackCredentials({ botToken, userToken, teamName, userName }) {
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
config.slack = { botToken, userToken, teamName, userName };
|
|
40
|
+
saveConfig(config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function saveJobs(jobs) {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
config.jobs = jobs;
|
|
46
|
+
saveConfig(config);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getJobs() {
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
return config.jobs || {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isConfigured() {
|
|
55
|
+
const creds = getCredentials();
|
|
56
|
+
return !!(creds.jenkins && creds.slack);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clearAll() {
|
|
60
|
+
if (fs.existsSync(CONFIG_FILE)) fs.unlinkSync(CONFIG_FILE);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { getCredentials, saveJenkinsCredentials, saveSlackCredentials, saveJobs, getJobs, isConfigured, clearAll };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
async function validateAndFetchUser(baseUrl, user, apiToken) {
|
|
4
|
+
const resp = await axios.get(`${baseUrl}/me/api/json`, {
|
|
5
|
+
auth: { username: user, password: apiToken },
|
|
6
|
+
});
|
|
7
|
+
return { fullName: resp.data.fullName, id: resp.data.id };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function fetchJobs(baseUrl, user, apiToken) {
|
|
11
|
+
const resp = await axios.get(`${baseUrl}/api/json?tree=jobs[name,url,color]`, {
|
|
12
|
+
auth: { username: user, password: apiToken },
|
|
13
|
+
});
|
|
14
|
+
return resp.data.jobs || [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function triggerBuild({ baseUrl, user, apiToken, buildToken, jobPath, branch }) {
|
|
18
|
+
const url = `${baseUrl}${jobPath}/buildWithParameters`;
|
|
19
|
+
await axios.post(url, null, {
|
|
20
|
+
params: { token: buildToken, BRANCH: branch, cause: `MCP trigger for ${branch}` },
|
|
21
|
+
auth: { username: user, password: apiToken },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { validateAndFetchUser, fetchJobs, triggerBuild };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const open = require('open');
|
|
4
|
+
const { saveSlackCredentials } = require('./credentials');
|
|
5
|
+
|
|
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;
|
|
64
|
+
}
|
|
65
|
+
|
|
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
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { slackOAuthFlow, getSlackUserInfo, postToSlack };
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
3
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
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');
|
|
8
|
+
|
|
9
|
+
const server = new Server({ name: 'jenkins-slack-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
10
|
+
|
|
11
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
12
|
+
tools: [
|
|
13
|
+
{
|
|
14
|
+
name: 'login_jenkins',
|
|
15
|
+
description: 'Login to Jenkins. Validates credentials and stores them securely in ~/.jenkins-slack-mcp/',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
baseUrl: { type: 'string', description: 'Jenkins base URL (e.g. https://ci-ind.gopando.in)' },
|
|
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' },
|
|
23
|
+
},
|
|
24
|
+
required: ['baseUrl', 'user', 'apiToken', 'buildToken'],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'login_slack',
|
|
29
|
+
description: 'Login to Slack via OAuth. Opens browser for authentication.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
clientId: { type: 'string', description: 'Slack App Client ID' },
|
|
34
|
+
clientSecret: { type: 'string', description: 'Slack App Client Secret' },
|
|
35
|
+
},
|
|
36
|
+
required: ['clientId', 'clientSecret'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'status',
|
|
41
|
+
description: 'Check current login status for Jenkins and Slack',
|
|
42
|
+
inputSchema: { type: 'object', properties: {} },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'add_job',
|
|
46
|
+
description: 'Register a Jenkins job that can be triggered',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
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' },
|
|
54
|
+
},
|
|
55
|
+
required: ['command', 'jobPath', 'name'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'list_jobs',
|
|
60
|
+
description: 'List all registered Jenkins jobs',
|
|
61
|
+
inputSchema: { type: 'object', properties: {} },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'trigger_build',
|
|
65
|
+
description: 'Trigger a Jenkins build and optionally notify Slack',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
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)' },
|
|
72
|
+
},
|
|
73
|
+
required: ['job'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'whoami',
|
|
78
|
+
description: 'Get current Jenkins and Slack user details',
|
|
79
|
+
inputSchema: { type: 'object', properties: {} },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'logout',
|
|
83
|
+
description: 'Clear all stored credentials',
|
|
84
|
+
inputSchema: { type: 'object', properties: {} },
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
90
|
+
const { name, arguments: args } = request.params;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
switch (name) {
|
|
94
|
+
case 'login_jenkins': {
|
|
95
|
+
const userInfo = await validateAndFetchUser(args.baseUrl, args.user, args.apiToken);
|
|
96
|
+
saveJenkinsCredentials(args);
|
|
97
|
+
return text(`✅ Jenkins login successful! Welcome, ${userInfo.fullName} (${userInfo.id})`);
|
|
98
|
+
}
|
|
99
|
+
|
|
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}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'status': {
|
|
113
|
+
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}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'add_job': {
|
|
120
|
+
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'})`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case 'list_jobs': {
|
|
127
|
+
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);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'trigger_build': {
|
|
134
|
+
const creds = getCredentials();
|
|
135
|
+
if (!creds.jenkins) return text('❌ Not logged into Jenkins. Use login_jenkins first.');
|
|
136
|
+
|
|
137
|
+
const jobs = getJobs();
|
|
138
|
+
const job = jobs[args.job];
|
|
139
|
+
if (!job) return text(`❌ Unknown job: ${args.job}. Available: ${Object.keys(jobs).join(', ')}`);
|
|
140
|
+
|
|
141
|
+
const branch = args.branch || job.defaultBranch;
|
|
142
|
+
await triggerBuild({
|
|
143
|
+
baseUrl: creds.jenkins.baseUrl,
|
|
144
|
+
user: creds.jenkins.user,
|
|
145
|
+
apiToken: creds.jenkins.apiToken,
|
|
146
|
+
buildToken: creds.jenkins.buildToken,
|
|
147
|
+
jobPath: job.path,
|
|
148
|
+
branch,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let msg = `✅ *${job.name}* build triggered on \`${branch}\``;
|
|
152
|
+
|
|
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}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return text(msg);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'whoami': {
|
|
163
|
+
const creds = getCredentials();
|
|
164
|
+
const parts = [];
|
|
165
|
+
if (creds.jenkins) {
|
|
166
|
+
const userInfo = await validateAndFetchUser(creds.jenkins.baseUrl, creds.jenkins.user, creds.jenkins.apiToken);
|
|
167
|
+
parts.push(`Jenkins: ${userInfo.fullName} (${userInfo.id}) @ ${creds.jenkins.baseUrl}`);
|
|
168
|
+
} else {
|
|
169
|
+
parts.push('Jenkins: not logged in');
|
|
170
|
+
}
|
|
171
|
+
if (creds.slack?.botToken) {
|
|
172
|
+
const slackInfo = await getSlackUserInfo(creds.slack.botToken);
|
|
173
|
+
parts.push(`Slack: ${slackInfo.user} @ ${slackInfo.team}`);
|
|
174
|
+
} else {
|
|
175
|
+
parts.push('Slack: not logged in');
|
|
176
|
+
}
|
|
177
|
+
return text(parts.join('\n'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'logout': {
|
|
181
|
+
clearAll();
|
|
182
|
+
return text('✅ All credentials cleared.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
return text(`Unknown tool: ${name}`);
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return { content: [{ type: 'text', text: `❌ Error: ${err.message}` }], isError: true };
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function text(msg) {
|
|
194
|
+
return { content: [{ type: 'text', text: msg }] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function main() {
|
|
198
|
+
const transport = new StdioServerTransport();
|
|
199
|
+
await server.connect(transport);
|
|
200
|
+
console.error('Jenkins-Slack MCP server running');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jenkins-slack-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server to trigger Jenkins builds from any IDE with Slack notifications. Auto-configures via interactive login.",
|
|
5
|
+
"main": "mcp-server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jenkins-slack-mcp": "mcp-server.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node mcp-server.js",
|
|
11
|
+
"server": "node server.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"jenkins",
|
|
16
|
+
"slack",
|
|
17
|
+
"ci",
|
|
18
|
+
"build",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"amazon-q",
|
|
21
|
+
"ide"
|
|
22
|
+
],
|
|
23
|
+
"author": "Seetharam1999 (https://github.com/Seetharam1999)",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": ""
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
|
+
"axios": "^1.6.0",
|
|
32
|
+
"dotenv": "^16.3.1",
|
|
33
|
+
"express": "^4.18.2",
|
|
34
|
+
"open": "^9.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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}`));
|