Skip to content

Commit 019dba6

Browse files
authored
chore: add Claude Code plugin configuration (#333)
1 parent 935d308 commit 019dba6

File tree

3 files changed

+395
-0
lines changed

3 files changed

+395
-0
lines changed

.claude-plugin/install-binary.mjs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env node
2+
3+
import { spawn } from 'node:child_process';
4+
import { createHash } from 'node:crypto';
5+
import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
6+
import { tmpdir } from 'node:os';
7+
import { join } from 'node:path';
8+
import process from 'node:process';
9+
import { pipeline } from 'node:stream/promises';
10+
11+
const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT;
12+
if (!PLUGIN_ROOT) {
13+
console.error('Error: CLAUDE_PLUGIN_ROOT environment variable not set');
14+
process.exit(1);
15+
}
16+
17+
// Detect OS and architecture
18+
const platform = process.platform;
19+
const arch = process.arch;
20+
21+
let OS, ARCH, EXT, BINARY_NAME, BINARY_PATH;
22+
23+
// Map Node.js arch to release naming
24+
switch (arch) {
25+
case 'x64':
26+
ARCH = 'x86_64';
27+
break;
28+
case 'arm64':
29+
ARCH = 'arm64';
30+
break;
31+
case 'ia32':
32+
ARCH = 'i386';
33+
break;
34+
default:
35+
console.error(`Unsupported architecture: ${arch}`);
36+
process.exit(1);
37+
}
38+
39+
// Map Node.js platform to release naming
40+
switch (platform) {
41+
case 'darwin':
42+
OS = 'Darwin';
43+
EXT = 'tar.gz';
44+
BINARY_NAME = 'mcp-grafana';
45+
break;
46+
case 'linux':
47+
OS = 'Linux';
48+
EXT = 'tar.gz';
49+
BINARY_NAME = 'mcp-grafana';
50+
break;
51+
case 'win32':
52+
OS = 'Windows';
53+
EXT = 'zip';
54+
BINARY_NAME = 'mcp-grafana.exe';
55+
break;
56+
default:
57+
console.error(`Unsupported OS: ${platform}`);
58+
process.exit(1);
59+
}
60+
61+
BINARY_PATH = join(PLUGIN_ROOT, BINARY_NAME);
62+
63+
// Fetch latest version from GitHub API
64+
async function getLatestVersion() {
65+
const headers = {};
66+
// Use GitHub token if available (for CI environments)
67+
if (process.env.GITHUB_TOKEN) {
68+
headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`;
69+
}
70+
71+
const response = await fetch('https://api.github.com/repos/grafana/mcp-grafana/releases/latest', {
72+
headers
73+
});
74+
if (!response.ok) {
75+
throw new Error(`Failed to fetch latest version: ${response.statusText}`);
76+
}
77+
const data = await response.json();
78+
return data.tag_name;
79+
}
80+
81+
// Download file from URL
82+
async function downloadFile(url, destPath) {
83+
const response = await fetch(url);
84+
if (!response.ok) {
85+
throw new Error(`Failed to download ${url}: ${response.statusText}`);
86+
}
87+
await pipeline(response.body, createWriteStream(destPath));
88+
}
89+
90+
// Verify SHA256 checksum
91+
async function verifyChecksum(filePath, checksumsContent, archiveName) {
92+
const fileBuffer = readFileSync(filePath);
93+
const hash = createHash('sha256').update(fileBuffer).digest('hex');
94+
95+
const lines = checksumsContent.split('\n');
96+
for (const line of lines) {
97+
if (line.includes(archiveName)) {
98+
const [expectedHash] = line.split(/\s+/);
99+
if (hash === expectedHash) {
100+
console.error(`✓ Checksum verified`);
101+
return true;
102+
} else {
103+
throw new Error(`Checksum mismatch for ${archiveName}`);
104+
}
105+
}
106+
}
107+
throw new Error(`No checksum found for ${archiveName}`);
108+
}
109+
110+
// Extract tar.gz archive using system tar command
111+
function extractTarGz(archivePath, destDir) {
112+
return new Promise((resolve, reject) => {
113+
const tar = spawn('tar', ['-xzf', archivePath, '-C', destDir]);
114+
tar.on('close', (code) => {
115+
if (code === 0) resolve();
116+
else reject(new Error(`tar extraction failed with code ${code}`));
117+
});
118+
});
119+
}
120+
121+
// Extract zip archive using system command
122+
function extractZip(archivePath, destDir) {
123+
return new Promise((resolve, reject) => {
124+
// Use PowerShell on Windows
125+
const unzip = spawn('powershell', ['-Command', `Expand-Archive -Path "${archivePath}" -DestinationPath "${destDir}" -Force`]);
126+
unzip.on('close', (code) => {
127+
if (code === 0) resolve();
128+
else reject(new Error(`zip extraction failed with code ${code}`));
129+
});
130+
});
131+
}
132+
133+
async function main() {
134+
try {
135+
// Get latest version
136+
console.error('Fetching latest version...');
137+
const VERSION = await getLatestVersion();
138+
139+
const ARCHIVE_NAME = `mcp-grafana_${OS}_${ARCH}.${EXT}`;
140+
const VERSION_FILE = join(PLUGIN_ROOT, '.mcp-grafana-version');
141+
142+
// Check if binary exists and version matches
143+
const needsInstall = !existsSync(BINARY_PATH) ||
144+
!existsSync(VERSION_FILE) ||
145+
readFileSync(VERSION_FILE, 'utf8').trim() !== VERSION;
146+
147+
if (!needsInstall) {
148+
// Binary is up to date, just execute it
149+
const child = spawn(BINARY_PATH, process.argv.slice(2), { stdio: 'inherit' });
150+
child.on('exit', (code) => process.exit(code || 0));
151+
return;
152+
}
153+
154+
console.error(`Downloading mcp-grafana ${VERSION} for ${OS}-${ARCH}...`);
155+
156+
// Create temp directory
157+
const TEMP_DIR = join(tmpdir(), `mcp-grafana-${Date.now()}`);
158+
mkdirSync(TEMP_DIR, { recursive: true });
159+
160+
try {
161+
const ARCHIVE_PATH = join(TEMP_DIR, ARCHIVE_NAME);
162+
const DOWNLOAD_URL = `https://github.com/grafana/mcp-grafana/releases/latest/download/${ARCHIVE_NAME}`;
163+
164+
// Download archive
165+
await downloadFile(DOWNLOAD_URL, ARCHIVE_PATH);
166+
167+
// Download and verify checksums
168+
console.error('Verifying checksum...');
169+
const VERSION_NUMBER = VERSION.replace(/^v/, ''); // Remove 'v' prefix
170+
const CHECKSUMS_URL = `https://github.com/grafana/mcp-grafana/releases/download/${VERSION}/mcp-grafana_${VERSION_NUMBER}_checksums.txt`;
171+
const checksumResponse = await fetch(CHECKSUMS_URL);
172+
if (!checksumResponse.ok) {
173+
throw new Error(`Failed to download checksums: ${checksumResponse.statusText}`);
174+
}
175+
const checksumsContent = await checksumResponse.text();
176+
await verifyChecksum(ARCHIVE_PATH, checksumsContent, ARCHIVE_NAME);
177+
178+
// Extract archive
179+
console.error('Extracting archive...');
180+
if (EXT === 'tar.gz') {
181+
await extractTarGz(ARCHIVE_PATH, TEMP_DIR);
182+
} else {
183+
await extractZip(ARCHIVE_PATH, TEMP_DIR);
184+
}
185+
186+
// Move binary to plugin root
187+
const extractedBinary = join(TEMP_DIR, BINARY_NAME);
188+
if (!existsSync(extractedBinary)) {
189+
throw new Error(`Binary not found after extraction: ${extractedBinary}`);
190+
}
191+
192+
mkdirSync(PLUGIN_ROOT, { recursive: true });
193+
const binaryContent = readFileSync(extractedBinary);
194+
writeFileSync(BINARY_PATH, binaryContent);
195+
196+
if (platform !== 'win32') {
197+
chmodSync(BINARY_PATH, 0o755);
198+
}
199+
200+
writeFileSync(VERSION_FILE, VERSION);
201+
202+
console.error(`Successfully installed mcp-grafana ${VERSION}`);
203+
} finally {
204+
// Cleanup temp directory
205+
try {
206+
const { rmSync } = await import('fs');
207+
rmSync(TEMP_DIR, { recursive: true, force: true });
208+
} catch (e) {
209+
// Ignore cleanup errors
210+
}
211+
}
212+
213+
// Execute the binary
214+
const child = spawn(BINARY_PATH, process.argv.slice(2), { stdio: 'inherit' });
215+
child.on('exit', (code) => process.exit(code || 0));
216+
217+
} catch (error) {
218+
console.error(`Error: ${error.message}`);
219+
process.exit(1);
220+
}
221+
}
222+
223+
main();

.claude-plugin/plugin.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "grafana",
3+
"version": "0.7.6",
4+
"description": "A Model Context Protocol (MCP) server for Grafana providing access to dashboards, datasources, and querying capabilities",
5+
"author": {
6+
"name": "Grafana Labs"
7+
},
8+
"homepage": "https://github.com/grafana/mcp-grafana",
9+
"repository": "https://github.com/grafana/mcp-grafana",
10+
"license": "Apache-2.0",
11+
"mcpServers": {
12+
"grafana": {
13+
"command": "node",
14+
"args": ["${CLAUDE_PLUGIN_ROOT}/.claude-plugin/install-binary.mjs"]
15+
}
16+
}
17+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
name: Test Install Scripts
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '.claude-plugin/**'
7+
- '.github/workflows/test-install-scripts.yml'
8+
push:
9+
branches:
10+
- main
11+
paths:
12+
- '.claude-plugin/**'
13+
- '.github/workflows/test-install-scripts.yml'
14+
workflow_dispatch:
15+
16+
jobs:
17+
test-nodejs-script:
18+
name: Test Node.js Script
19+
runs-on: ${{ matrix.os }}
20+
strategy:
21+
matrix:
22+
os: [ubuntu-latest, macos-latest, windows-latest]
23+
node-version: ['20', '22', '24']
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: ${{ matrix.node-version }}
32+
33+
- name: Test Node.js install script (Unix)
34+
if: runner.os != 'Windows'
35+
shell: bash
36+
env:
37+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38+
run: |
39+
TEMP_DIR=$(mktemp -d)
40+
export CLAUDE_PLUGIN_ROOT="${TEMP_DIR}"
41+
42+
echo "Testing Node.js install script..."
43+
node .claude-plugin/install-binary.mjs --version
44+
45+
VERSION=$("${TEMP_DIR}/mcp-grafana" --version)
46+
echo "Installed version: ${VERSION}"
47+
48+
if [ -z "${VERSION}" ]; then
49+
echo "Error: Failed to get version"
50+
exit 1
51+
fi
52+
53+
echo "✓ Node.js script test passed"
54+
rm -rf "${TEMP_DIR}"
55+
56+
- name: Test Node.js install script (Windows)
57+
if: runner.os == 'Windows'
58+
shell: pwsh
59+
env:
60+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61+
run: |
62+
$TempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ([System.IO.Path]::GetRandomFileName()))
63+
$env:CLAUDE_PLUGIN_ROOT = $TempDir.FullName
64+
65+
Write-Host "Testing Node.js install script..."
66+
node .claude-plugin/install-binary.mjs --version
67+
68+
$BinaryPath = Join-Path $TempDir.FullName "mcp-grafana.exe"
69+
$Version = & $BinaryPath --version
70+
Write-Host "Installed version: $Version"
71+
72+
if ([string]::IsNullOrEmpty($Version)) {
73+
Write-Error "Failed to get version"
74+
exit 1
75+
}
76+
77+
Write-Host "✓ Node.js script test passed"
78+
Remove-Item -Recurse -Force $TempDir
79+
80+
test-checksum-verification:
81+
name: Test Checksum Verification
82+
runs-on: ubuntu-latest
83+
steps:
84+
- name: Checkout code
85+
uses: actions/checkout@v4
86+
87+
- name: Setup Node.js
88+
uses: actions/setup-node@v4
89+
with:
90+
node-version: '22'
91+
92+
- name: Test checksum verification with Node.js
93+
shell: bash
94+
env:
95+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96+
run: |
97+
TEMP_DIR=$(mktemp -d)
98+
export CLAUDE_PLUGIN_ROOT="${TEMP_DIR}"
99+
100+
echo "Testing checksum verification..."
101+
OUTPUT=$(node .claude-plugin/install-binary.mjs --version 2>&1)
102+
echo "Script output:"
103+
echo "$OUTPUT"
104+
105+
if echo "$OUTPUT" | grep -q "Checksum verified"; then
106+
echo "✓ Checksum verification executed"
107+
else
108+
echo "Error: Checksum verification not executed"
109+
exit 1
110+
fi
111+
112+
rm -rf "${TEMP_DIR}"
113+
114+
test-version-update:
115+
name: Test Version Update Detection
116+
runs-on: ubuntu-latest
117+
steps:
118+
- name: Checkout code
119+
uses: actions/checkout@v4
120+
121+
- name: Setup Node.js
122+
uses: actions/setup-node@v4
123+
with:
124+
node-version: '22'
125+
126+
- name: Test version update detection
127+
shell: bash
128+
env:
129+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+
run: |
131+
TEMP_DIR=$(mktemp -d)
132+
export CLAUDE_PLUGIN_ROOT="${TEMP_DIR}"
133+
134+
echo "First installation..."
135+
node .claude-plugin/install-binary.mjs --version
136+
137+
VERSION_FILE="${TEMP_DIR}/.mcp-grafana-version"
138+
if [ ! -f "${VERSION_FILE}" ]; then
139+
echo "Error: Version file not created"
140+
exit 1
141+
fi
142+
143+
STORED_VERSION=$(cat "${VERSION_FILE}")
144+
echo "Stored version: ${STORED_VERSION}"
145+
146+
echo "Second run (should skip download)..."
147+
OUTPUT=$(node .claude-plugin/install-binary.mjs --version 2>&1)
148+
149+
if echo "${OUTPUT}" | grep -q "Downloading"; then
150+
echo "Error: Should not download on second run"
151+
exit 1
152+
fi
153+
154+
echo "✓ Version update detection works"
155+
rm -rf "${TEMP_DIR}"

0 commit comments

Comments
 (0)