jira-dash-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/LICENSE +21 -0
- package/README.md +110 -0
- package/bin/jira-grafana-dashboard-mcp.js +254 -0
- package/package.json +33 -0
- package/vendor/grafana-server.js +911 -0
- package/vendor/mysql-server.js +388 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Jira Grafana Dashboard MCP
|
|
2
|
+
|
|
3
|
+
A single MCP server for Claude Code that combines:
|
|
4
|
+
|
|
5
|
+
- read-only MySQL tools for Jira data synchronized into MySQL
|
|
6
|
+
- Grafana dashboard tools for datasource discovery, dashboard validation, and `update_dashboard`
|
|
7
|
+
|
|
8
|
+
This package contains **no company-specific configuration**. Runtime values are provided through environment variables in the caller's `.mcp.json`.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Node.js >= 18
|
|
13
|
+
- Claude Code
|
|
14
|
+
- A MySQL account with read-only access to the Jira synchronized database
|
|
15
|
+
- A Grafana service account token with dashboard read/create/write permissions and datasource read permission
|
|
16
|
+
|
|
17
|
+
## Claude `.mcp.json` example
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"jira-grafana-dashboard-tools": {
|
|
23
|
+
"type": "stdio",
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": [
|
|
26
|
+
"-y",
|
|
27
|
+
"jira-dash-mcp@latest"
|
|
28
|
+
],
|
|
29
|
+
"env": {
|
|
30
|
+
"MYSQL_HOST": "your-mysql-host",
|
|
31
|
+
"MYSQL_PORT": "3306",
|
|
32
|
+
"MYSQL_USER": "readonly_user",
|
|
33
|
+
"MYSQL_PASSWORD": "your-password",
|
|
34
|
+
"MYSQL_DATABASE": "your-database",
|
|
35
|
+
"MYSQL_DEFAULT_ROW_LIMIT": "200",
|
|
36
|
+
"MYSQL_MAX_ROW_LIMIT": "2000",
|
|
37
|
+
"MYSQL_QUERY_TIMEOUT_MS": "30000",
|
|
38
|
+
|
|
39
|
+
"GRAFANA_URL": "https://your-grafana.example.com",
|
|
40
|
+
"GRAFANA_TOKEN": "your-service-account-token",
|
|
41
|
+
"GRAFANA_DATASOURCE_UID": "your-mysql-datasource-uid",
|
|
42
|
+
"GRAFANA_FOLDER_UID": "your-target-folder-uid",
|
|
43
|
+
"DASHBOARD_STATE_FILE": ".runtime/last-dashboard.json"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Tool naming
|
|
51
|
+
|
|
52
|
+
The package exposes one MCP server, with prefixed tools:
|
|
53
|
+
|
|
54
|
+
MySQL tools:
|
|
55
|
+
|
|
56
|
+
- `mysql_get_config`
|
|
57
|
+
- `mysql_healthcheck`
|
|
58
|
+
- `mysql_list_tables`
|
|
59
|
+
- `mysql_describe_table`
|
|
60
|
+
- `mysql_sample_rows`
|
|
61
|
+
- `mysql_query`
|
|
62
|
+
- `mysql_validate_sql`
|
|
63
|
+
- `mysql_jira_schema_summary`
|
|
64
|
+
|
|
65
|
+
Grafana tools:
|
|
66
|
+
|
|
67
|
+
- `grafana_get_config`
|
|
68
|
+
- `grafana_healthcheck`
|
|
69
|
+
- `grafana_list_datasources`
|
|
70
|
+
- `grafana_get_datasource`
|
|
71
|
+
- `grafana_search_folders`
|
|
72
|
+
- `grafana_search_dashboards`
|
|
73
|
+
- `grafana_get_dashboard_by_uid`
|
|
74
|
+
- `grafana_get_dashboard_summary`
|
|
75
|
+
- `grafana_get_dashboard_property`
|
|
76
|
+
- `grafana_validate_dashboard`
|
|
77
|
+
- `grafana_update_dashboard`
|
|
78
|
+
- `grafana_generate_deeplink`
|
|
79
|
+
- `grafana_get_last_dashboard`
|
|
80
|
+
- `grafana_save_dashboard_state`
|
|
81
|
+
- `grafana_clear_dashboard_state`
|
|
82
|
+
- `grafana_validate_sql`
|
|
83
|
+
|
|
84
|
+
## Dependency behavior
|
|
85
|
+
|
|
86
|
+
This package declares `mysql2` under `dependencies`, so users do **not** run `npm install mysql2` manually. When Claude runs this package through `npx`, npm resolves and installs runtime dependencies automatically.
|
|
87
|
+
|
|
88
|
+
## Security notes
|
|
89
|
+
|
|
90
|
+
- MySQL execution is read-only and validates SQL before execution.
|
|
91
|
+
- `mysql_query` only allows read-only statements such as SELECT/WITH/SHOW/DESCRIBE/EXPLAIN.
|
|
92
|
+
- Dashboard SQL is validated before write.
|
|
93
|
+
- Do not publish `.env`, `.mcp.json`, real tokens, real database names, real IPs, or internal schema files to npm.
|
|
94
|
+
|
|
95
|
+
## Local smoke test before publishing
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm install
|
|
99
|
+
npm run smoke
|
|
100
|
+
npm pack --dry-run
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Publish
|
|
104
|
+
|
|
105
|
+
For a scoped public package:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm login
|
|
109
|
+
npm publish --access public
|
|
110
|
+
```
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Unified Jira Grafana Dashboard MCP Server
|
|
4
|
+
*
|
|
5
|
+
* This process exposes one MCP server to Claude Code and internally delegates to
|
|
6
|
+
* two self-contained child MCP servers:
|
|
7
|
+
* - vendor/mysql-server.js -> tools are exposed as mysql_*
|
|
8
|
+
* - vendor/grafana-server.js -> tools are exposed as grafana_*
|
|
9
|
+
*
|
|
10
|
+
* It intentionally contains no company-specific configuration. All runtime
|
|
11
|
+
* configuration must be provided through environment variables in .mcp.json.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { spawn } = require("child_process");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const VERSION = "1.0.0";
|
|
18
|
+
const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
|
|
19
|
+
|
|
20
|
+
function response(id, result) {
|
|
21
|
+
return { jsonrpc: "2.0", id, result };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function errorResponse(id, code, message, data) {
|
|
25
|
+
return { jsonrpc: "2.0", id, error: { code, message, data } };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeMessage(msg) {
|
|
29
|
+
if (!msg) return;
|
|
30
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class ChildMcpClient {
|
|
34
|
+
constructor({ name, prefix, script }) {
|
|
35
|
+
this.name = name;
|
|
36
|
+
this.prefix = prefix;
|
|
37
|
+
this.script = script;
|
|
38
|
+
this.proc = null;
|
|
39
|
+
this.buffer = Buffer.alloc(0);
|
|
40
|
+
this.nextId = 1;
|
|
41
|
+
this.pending = new Map();
|
|
42
|
+
this.initialized = false;
|
|
43
|
+
this.tools = null;
|
|
44
|
+
this.stderr = "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
start() {
|
|
48
|
+
if (this.proc) return;
|
|
49
|
+
this.proc = spawn(process.execPath, [this.script], {
|
|
50
|
+
env: process.env,
|
|
51
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.proc.stdout.on("data", (chunk) => {
|
|
55
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
56
|
+
this.parseMessages();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
60
|
+
const text = chunk.toString("utf8");
|
|
61
|
+
this.stderr += text;
|
|
62
|
+
if (this.stderr.length > 4000) this.stderr = this.stderr.slice(-4000);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.proc.on("exit", (code, signal) => {
|
|
66
|
+
const err = new Error(`${this.name} exited with code=${code} signal=${signal}${this.stderr ? ` stderr=${this.stderr}` : ""}`);
|
|
67
|
+
for (const { reject } of this.pending.values()) reject(err);
|
|
68
|
+
this.pending.clear();
|
|
69
|
+
this.proc = null;
|
|
70
|
+
this.initialized = false;
|
|
71
|
+
this.tools = null;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
parseMessages() {
|
|
76
|
+
while (this.buffer.length > 0) {
|
|
77
|
+
const headerEnd = this.buffer.indexOf(Buffer.from("\r\n\r\n"));
|
|
78
|
+
if (headerEnd !== -1) {
|
|
79
|
+
const header = this.buffer.slice(0, headerEnd).toString("utf8");
|
|
80
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
81
|
+
if (!match) throw new Error(`Invalid ${this.name} MCP header: ${header}`);
|
|
82
|
+
const length = Number(match[1]);
|
|
83
|
+
const bodyStart = headerEnd + 4;
|
|
84
|
+
const bodyEnd = bodyStart + length;
|
|
85
|
+
if (this.buffer.length < bodyEnd) return;
|
|
86
|
+
const body = this.buffer.slice(bodyStart, bodyEnd).toString("utf8");
|
|
87
|
+
this.buffer = this.buffer.slice(bodyEnd);
|
|
88
|
+
this.handleChildMessage(JSON.parse(body));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nl = this.buffer.indexOf(Buffer.from("\n"));
|
|
93
|
+
if (nl === -1) return;
|
|
94
|
+
const line = this.buffer.slice(0, nl).toString("utf8").trim();
|
|
95
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
96
|
+
if (!line) continue;
|
|
97
|
+
if (!line.startsWith("{")) continue;
|
|
98
|
+
this.handleChildMessage(JSON.parse(line));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
handleChildMessage(msg) {
|
|
103
|
+
if (!msg || msg.id === undefined || msg.id === null) return;
|
|
104
|
+
const entry = this.pending.get(msg.id);
|
|
105
|
+
if (!entry) return;
|
|
106
|
+
clearTimeout(entry.timer);
|
|
107
|
+
this.pending.delete(msg.id);
|
|
108
|
+
if (msg.error) entry.reject(new Error(`${this.name}: ${msg.error.message || JSON.stringify(msg.error)}`));
|
|
109
|
+
else entry.resolve(msg.result);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
request(method, params = {}, timeoutMs = 30000) {
|
|
113
|
+
this.start();
|
|
114
|
+
const id = this.nextId++;
|
|
115
|
+
const payload = { jsonrpc: "2.0", id, method, params };
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
this.pending.delete(id);
|
|
119
|
+
reject(new Error(`${this.name} MCP request timeout: ${method}${this.stderr ? ` stderr=${this.stderr}` : ""}`));
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
122
|
+
this.proc.stdin.write(JSON.stringify(payload) + "\n");
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async ensureInitialized() {
|
|
127
|
+
if (this.initialized) return;
|
|
128
|
+
await this.request("initialize", {
|
|
129
|
+
protocolVersion: DEFAULT_PROTOCOL_VERSION,
|
|
130
|
+
capabilities: {},
|
|
131
|
+
clientInfo: { name: "jira-grafana-dashboard-mcp-wrapper", version: VERSION },
|
|
132
|
+
});
|
|
133
|
+
if (this.proc && this.proc.stdin.writable) {
|
|
134
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }) + "\n");
|
|
135
|
+
}
|
|
136
|
+
this.initialized = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async listTools() {
|
|
140
|
+
await this.ensureInitialized();
|
|
141
|
+
if (!this.tools) {
|
|
142
|
+
const result = await this.request("tools/list", {});
|
|
143
|
+
this.tools = (result.tools || []).map((tool) => ({
|
|
144
|
+
...tool,
|
|
145
|
+
name: `${this.prefix}_${tool.name}`,
|
|
146
|
+
description: `[${this.name}] ${tool.description || ""}`,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
return this.tools;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async callTool(prefixedName, args) {
|
|
153
|
+
await this.ensureInitialized();
|
|
154
|
+
const rawName = prefixedName.slice(this.prefix.length + 1);
|
|
155
|
+
return await this.request("tools/call", { name: rawName, arguments: args || {} }, Number(process.env.MCP_CHILD_TOOL_TIMEOUT_MS || 120000));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
stop() {
|
|
159
|
+
if (this.proc) this.proc.kill();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const root = path.resolve(__dirname, "..");
|
|
164
|
+
const children = [
|
|
165
|
+
new ChildMcpClient({
|
|
166
|
+
name: "mysql",
|
|
167
|
+
prefix: "mysql",
|
|
168
|
+
script: path.join(root, "vendor", "mysql-server.js"),
|
|
169
|
+
}),
|
|
170
|
+
new ChildMcpClient({
|
|
171
|
+
name: "grafana",
|
|
172
|
+
prefix: "grafana",
|
|
173
|
+
script: path.join(root, "vendor", "grafana-server.js"),
|
|
174
|
+
}),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
function findChildForTool(name) {
|
|
178
|
+
return children.find((child) => name && name.startsWith(`${child.prefix}_`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function handleMessage(msg) {
|
|
182
|
+
try {
|
|
183
|
+
if (!msg || msg.jsonrpc !== "2.0") return null;
|
|
184
|
+
const { id, method, params } = msg;
|
|
185
|
+
|
|
186
|
+
if (method === "initialize") {
|
|
187
|
+
return response(id, {
|
|
188
|
+
protocolVersion: (params && params.protocolVersion) || DEFAULT_PROTOCOL_VERSION,
|
|
189
|
+
capabilities: { tools: {} },
|
|
190
|
+
serverInfo: { name: "jira-grafana-dashboard-mcp", version: VERSION },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (method === "notifications/initialized") return null;
|
|
195
|
+
if (method === "ping") return response(id, {});
|
|
196
|
+
|
|
197
|
+
if (method === "tools/list") {
|
|
198
|
+
const lists = await Promise.all(children.map((child) => child.listTools()));
|
|
199
|
+
return response(id, { tools: lists.flat() });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (method === "tools/call") {
|
|
203
|
+
const name = params && params.name;
|
|
204
|
+
const args = (params && params.arguments) || {};
|
|
205
|
+
const child = findChildForTool(name);
|
|
206
|
+
if (!child) throw new Error(`Unknown tool: ${name}`);
|
|
207
|
+
return response(id, await child.callTool(name, args));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return errorResponse(id, -32601, `Method not found: ${method}`);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return response(msg && msg.id, {
|
|
213
|
+
content: [{ type: "text", text: `ERROR: ${err.message}` }],
|
|
214
|
+
isError: true,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let buffer = Buffer.alloc(0);
|
|
220
|
+
function tryParseMessages() {
|
|
221
|
+
while (buffer.length > 0) {
|
|
222
|
+
const headerEnd = buffer.indexOf(Buffer.from("\r\n\r\n"));
|
|
223
|
+
if (headerEnd !== -1) {
|
|
224
|
+
const header = buffer.slice(0, headerEnd).toString("utf8");
|
|
225
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
226
|
+
if (!match) throw new Error(`Invalid MCP header: ${header}`);
|
|
227
|
+
const length = Number(match[1]);
|
|
228
|
+
const bodyStart = headerEnd + 4;
|
|
229
|
+
const bodyEnd = bodyStart + length;
|
|
230
|
+
if (buffer.length < bodyEnd) return;
|
|
231
|
+
const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
|
|
232
|
+
buffer = buffer.slice(bodyEnd);
|
|
233
|
+
handleMessage(JSON.parse(body)).then(writeMessage).catch((err) => writeMessage(errorResponse(null, -32603, err.message)));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const nl = buffer.indexOf(Buffer.from("\n"));
|
|
238
|
+
if (nl === -1) return;
|
|
239
|
+
const line = buffer.slice(0, nl).toString("utf8").trim();
|
|
240
|
+
buffer = buffer.slice(nl + 1);
|
|
241
|
+
if (!line) continue;
|
|
242
|
+
if (!line.startsWith("{")) continue;
|
|
243
|
+
handleMessage(JSON.parse(line)).then(writeMessage).catch((err) => writeMessage(errorResponse(null, -32603, err.message)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
process.stdin.on("data", (chunk) => {
|
|
248
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
249
|
+
try { tryParseMessages(); }
|
|
250
|
+
catch (err) { writeMessage(errorResponse(null, -32700, err.message)); }
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
process.on("SIGINT", () => { children.forEach((child) => child.stop()); process.exit(0); });
|
|
254
|
+
process.on("SIGTERM", () => { children.forEach((child) => child.stop()); process.exit(0); });
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jira-dash-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for read-only Jira MySQL data analysis and Grafana dashboard automation.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jira-dash-mcp": "bin/jira-grafana-dashboard-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"vendor",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"smoke": "node scripts/smoke-test.js",
|
|
17
|
+
"prepublishOnly": "node scripts/smoke-test.js && npm pack --dry-run"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"mysql2": "^3.11.5"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"jira",
|
|
28
|
+
"grafana",
|
|
29
|
+
"mysql",
|
|
30
|
+
"claude"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|