@vint.tri/report_gen_mcp 1.0.30 โ†’ 1.0.32

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/dist/index.js CHANGED
@@ -9,9 +9,11 @@ const commander_1 = require("commander");
9
9
  const reportGenerator_1 = require("./utils/reportGenerator");
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const os_1 = __importDefault(require("os"));
12
13
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
13
14
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
14
15
  const zod_1 = require("zod");
16
+ const url_1 = require("url");
15
17
  // Check if we're running in stdio mode (no command-line arguments)
16
18
  const isStdioMode = process.argv.length === 2;
17
19
  // For CLI and HTTP API modes, check for mandatory REPORTS_DIR environment variable
@@ -88,7 +90,7 @@ if (process.argv.length === 2) {
88
90
  // No command specified, run in stdio mode using MCP SDK
89
91
  const mcpServer = new mcp_js_1.McpServer({
90
92
  name: "report_gen_mcp",
91
- version: "1.0.29",
93
+ version: "1.0.31",
92
94
  }, {
93
95
  // Disable health check to prevent automatic calls
94
96
  capabilities: {
@@ -117,7 +119,7 @@ if (process.argv.length === 2) {
117
119
  }),
118
120
  })).describe("Chart configurations mapped by ID"),
119
121
  outputFile: zod_1.z.string().optional().describe("Output HTML file path"),
120
- tempDirectory: zod_1.z.string().describe("Temporary directory for file storage (required to prevent read-only file system errors)"),
122
+ tempDirectory: zod_1.z.string().optional().describe("Temporary directory for file storage (optional, will use REPORTS_DIR environment variable if set)"),
121
123
  },
122
124
  }, async (params) => {
123
125
  // Handle case where arguments might be sent as a JSON string by Claude desktop
@@ -133,16 +135,33 @@ if (process.argv.length === 2) {
133
135
  else if (params.arguments && typeof params.arguments === 'object') {
134
136
  processedParams = params.arguments;
135
137
  }
136
- // Check if tempDirectory parameter is provided
137
- if (!processedParams.tempDirectory) {
138
- throw new Error('tempDirectory parameter is required. Please provide the directory where reports should be generated.');
139
- }
140
138
  const { document, charts, outputFile = 'report.html', tempDirectory } = processedParams;
141
- const outputPath = path_1.default.resolve(tempDirectory, outputFile);
139
+ // Determine the output directory:
140
+ // 1. Use REPORTS_DIR environment variable if set
141
+ // 2. Fall back to tempDirectory parameter if provided
142
+ // 3. Default to system temp directory if neither is available
143
+ let outputDir;
144
+ if (process.env.REPORTS_DIR) {
145
+ outputDir = process.env.REPORTS_DIR;
146
+ // Ensure the reports directory exists
147
+ try {
148
+ fs_extra_1.default.ensureDirSync(outputDir);
149
+ }
150
+ catch (error) {
151
+ throw new Error(`Cannot create or access the reports directory: ${outputDir}`);
152
+ }
153
+ }
154
+ else if (tempDirectory) {
155
+ outputDir = tempDirectory;
156
+ }
157
+ else {
158
+ outputDir = os_1.default.tmpdir();
159
+ }
160
+ const outputPath = path_1.default.resolve(outputDir, outputFile);
142
161
  try {
143
162
  const result = await (0, reportGenerator_1.generateReport)(document, charts, outputPath);
144
- // Properly encode the file path for URL
145
- const encodedPath = encodeURIComponent(outputPath).replace(/%2F/g, '/');
163
+ // Generate proper file URL
164
+ const fileUrl = (0, url_1.pathToFileURL)(outputPath).href;
146
165
  return {
147
166
  content: [
148
167
  {
@@ -151,7 +170,7 @@ if (process.argv.length === 2) {
151
170
  ...result,
152
171
  message: "Report generated successfully",
153
172
  filePath: outputPath,
154
- fileUrl: `file://${encodedPath}`
173
+ fileUrl: fileUrl
155
174
  })
156
175
  }
157
176
  ]
@@ -172,9 +191,8 @@ if (process.argv.length === 2) {
172
191
  try {
173
192
  // Check if file exists
174
193
  await fs_extra_1.default.access(filePath);
175
- // Properly encode the file path for URL
176
- const encodedPath = encodeURIComponent(filePath).replace(/%2F/g, '/');
177
- const fileUrl = `file://${encodedPath}`;
194
+ // Generate proper file URL
195
+ const fileUrl = (0, url_1.pathToFileURL)(filePath).href;
178
196
  return {
179
197
  content: [
180
198
  {
@@ -1,164 +1,139 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Final verification test for report_gen_mcp v1.0.23
4
- // This test verifies that the tempDirectory parameter is properly handled
5
-
6
3
  const { spawn } = require('child_process');
7
4
  const fs = require('fs');
8
5
  const path = require('path');
9
6
 
10
- console.log('๐Ÿ” Final Verification Test for report_gen_mcp v1.0.23');
11
- console.log('=====================================================\n');
12
-
13
- // Test 1: Verify the tool works with tempDirectory parameter
14
- console.log('๐Ÿงช Test 1: Testing with tempDirectory parameter...');
15
- const testWithTempDir = spawn('npx', ['@vint.tri/report_gen_mcp@1.0.23'], {
16
- stdio: ['pipe', 'pipe', 'pipe'],
7
+ console.log('=== Final Verification Test ===\n');
8
+
9
+ // Test 1: CLI mode with REPORTS_DIR
10
+ console.log('Test 1: CLI mode with REPORTS_DIR environment variable...');
11
+ const testReportsDir = path.join(__dirname, 'test-reports');
12
+ if (!fs.existsSync(testReportsDir)) {
13
+ fs.mkdirSync(testReportsDir);
14
+ }
15
+
16
+ const cliTest = spawn('node', [
17
+ 'dist/index.js',
18
+ 'generate',
19
+ '--document', '# CLI Test Report\n\nThis is a test report with a [[chart:cli]] chart.',
20
+ '--charts', JSON.stringify({
21
+ cli: {
22
+ type: "bar",
23
+ config: {
24
+ labels: ["X", "Y", "Z"],
25
+ datasets: [{
26
+ label: "CLI Data",
27
+ data: [10, 20, 30],
28
+ backgroundColor: ["red", "green", "blue"]
29
+ }]
30
+ }
31
+ }
32
+ }),
33
+ '--output', 'cli-test-report.html'
34
+ ], {
35
+ env: {
36
+ ...process.env,
37
+ REPORTS_DIR: testReportsDir
38
+ }
17
39
  });
18
40
 
19
- let test1Output = '';
20
-
21
- testWithTempDir.stdout.on('data', (data) => {
22
- test1Output += data.toString();
41
+ cliTest.stdout.on('data', (data) => {
42
+ console.log('CLI stdout:', data.toString());
23
43
  });
24
44
 
25
- testWithTempDir.stderr.on('data', (data) => {
26
- test1Output += data.toString();
45
+ cliTest.stderr.on('data', (data) => {
46
+ console.log('CLI stderr:', data.toString());
27
47
  });
28
48
 
29
- testWithTempDir.on('close', (code) => {
30
- console.log(`Test 1 exited with code ${code}`);
31
- console.log('Output:');
32
- console.log(test1Output);
33
-
34
- // Send a generate-report request with tempDirectory
35
- const testRequest = {
36
- method: "generate-report",
37
- params: {
38
- document: "# Test Report\n\nThis is a test report with tempDirectory.\n\n[[chart:testChart]]",
39
- charts: {
40
- testChart: {
41
- type: "bar",
42
- config: {
43
- labels: ["A", "B", "C"],
44
- datasets: [
45
- {
46
- label: "Test Data",
47
- data: [1, 2, 3],
48
- backgroundColor: ["red", "green", "blue"]
49
- }
50
- ],
51
- options: {
52
- title: "Test Chart"
53
- }
54
- }
55
- }
56
- },
57
- outputFile: "test-report.html",
58
- tempDirectory: "/tmp"
49
+ cliTest.on('close', (code) => {
50
+ console.log('CLI test exited with code:', code);
51
+
52
+ const expectedReportPath = path.join(testReportsDir, 'cli-test-report.html');
53
+ if (fs.existsSync(expectedReportPath)) {
54
+ console.log('โœ… CLI Test PASSED: Report generated in correct directory\n');
55
+ } else {
56
+ console.log('โŒ CLI Test FAILED: Report not found in expected directory\n');
57
+ }
58
+
59
+ // Clean up
60
+ if (fs.existsSync(testReportsDir)) {
61
+ fs.rmSync(testReportsDir, { recursive: true, force: true });
62
+ }
63
+
64
+ // Test 2: CLI mode without REPORTS_DIR (should fail)
65
+ console.log('Test 2: CLI mode without REPORTS_DIR environment variable (should fail)...');
66
+ const cliErrorTest = spawn('node', [
67
+ 'dist/index.js',
68
+ 'generate',
69
+ '--document', '# Error Test Report',
70
+ '--charts', JSON.stringify({}),
71
+ '--output', 'error-test-report.html'
72
+ ], {
73
+ env: {
74
+ ...process.env,
75
+ REPORTS_DIR: ''
59
76
  }
60
- };
61
-
62
- console.log('\n๐Ÿ“ Sending test request with tempDirectory...');
63
- console.log(JSON.stringify(testRequest, null, 2));
64
-
65
- const testProcess = spawn('npx', ['@vint.tri/report_gen_mcp@1.0.23'], {
66
- stdio: ['pipe', 'pipe', 'pipe'],
67
77
  });
68
78
 
69
- let testOutput = '';
70
-
71
- testProcess.stdout.on('data', (data) => {
72
- testOutput += data.toString();
79
+ cliErrorTest.stdout.on('data', (data) => {
80
+ console.log('CLI Error stdout:', data.toString());
73
81
  });
74
82
 
75
- testProcess.stderr.on('data', (data) => {
76
- testOutput += data.toString();
83
+ cliErrorTest.stderr.on('data', (data) => {
84
+ console.log('CLI Error stderr:', data.toString());
77
85
  });
78
86
 
79
- testProcess.stdin.write(JSON.stringify(testRequest));
80
- testProcess.stdin.end();
81
-
82
- testProcess.on('close', (code) => {
83
- console.log(`\nโœ… Test completed with code ${code}`);
84
- console.log('Full output:');
85
- console.log(testOutput);
86
-
87
- // Try to parse the response
88
- try {
89
- const lines = testOutput.split('\n').filter(line => line.trim());
90
- const lastLine = lines[lines.length - 1];
91
- const response = JSON.parse(lastLine);
92
-
93
- if (response.success) {
94
- console.log('๐ŸŽ‰ SUCCESS: Report generated successfully!');
95
- console.log(`๐Ÿ“ File path: ${response.filePath}`);
96
-
97
- // Test 2: Verify the file exists and get-report-url works
98
- console.log('\n๐Ÿงช Test 2: Testing get-report-url...');
99
- const urlRequest = {
100
- method: "get-report-url",
101
- params: {
102
- filePath: response.filePath
103
- }
104
- };
105
-
106
- const urlProcess = spawn('npx', ['@vint.tri/report_gen_mcp@1.0.23'], {
107
- stdio: ['pipe', 'pipe', 'pipe'],
108
- });
109
-
110
- let urlOutput = '';
111
-
112
- urlProcess.stdout.on('data', (data) => {
113
- urlOutput += data.toString();
114
- });
115
-
116
- urlProcess.stderr.on('data', (data) => {
117
- urlOutput += data.toString();
118
- });
119
-
120
- urlProcess.stdin.write(JSON.stringify(urlRequest));
121
- urlProcess.stdin.end();
122
-
123
- urlProcess.on('close', (code) => {
124
- console.log(`\nโœ… URL test completed with code ${code}`);
125
- console.log('Full output:');
126
- console.log(urlOutput);
127
-
128
- try {
129
- const urlLines = urlOutput.split('\n').filter(line => line.trim());
130
- const urlLastLine = urlLines[urlLines.length - 1];
131
- const urlResponse = JSON.parse(urlLastLine);
132
-
133
- if (urlResponse.success) {
134
- console.log('๐ŸŽ‰ SUCCESS: get-report-url works correctly!');
135
- console.log(`๐Ÿ”— File URL: ${urlResponse.fileUrl}`);
136
- console.log('\n๐Ÿ† ALL TESTS PASSED! The fix is working correctly.');
137
- console.log('๐Ÿ“‹ Summary:');
138
- console.log(' - tempDirectory parameter is properly handled');
139
- console.log(' - Reports are generated successfully');
140
- console.log(' - File URLs are returned correctly');
141
- console.log(' - Version 1.0.23 is working as expected');
142
- } else {
143
- console.log('โŒ FAILED: get-report-url did not succeed');
144
- }
145
- } catch (e) {
146
- console.log('โŒ ERROR: Could not parse URL response');
147
- console.error(e);
148
- }
149
- });
150
- } else {
151
- console.log('โŒ FAILED: Report generation was not successful');
152
- console.log(response.message || 'Unknown error');
153
- }
154
- } catch (e) {
155
- console.log('โš ๏ธ WARNING: Could not parse response, but checking for success indicators...');
156
- if (testOutput.includes('success') && testOutput.includes('filePath')) {
157
- console.log('๐ŸŽ‰ SUCCESS: Found success indicators in output!');
87
+ cliErrorTest.on('close', (code) => {
88
+ console.log('CLI error test exited with code:', code);
89
+
90
+ if (code !== 0) {
91
+ console.log('โœ… CLI Error Test PASSED: Correctly failed without REPORTS_DIR\n');
92
+ } else {
93
+ console.log('โŒ CLI Error Test FAILED: Should have failed without REPORTS_DIR\n');
94
+ }
95
+
96
+ // Test 3: Stdio mode (should work without REPORTS_DIR)
97
+ console.log('Test 3: Stdio mode (should work without REPORTS_DIR)...');
98
+ const stdioTest = spawn('node', ['dist/index.js'], {
99
+ env: process.env
100
+ });
101
+
102
+ // Send a simple MCP request
103
+ const mcpRequest = {
104
+ "jsonrpc": "2.0",
105
+ "id": 1,
106
+ "method": "tools/list",
107
+ "params": {}
108
+ };
109
+
110
+ stdioTest.stdin.write(JSON.stringify(mcpRequest) + '\n');
111
+
112
+ let stdioOutput = '';
113
+ stdioTest.stdout.on('data', (data) => {
114
+ stdioOutput += data.toString();
115
+ });
116
+
117
+ stdioTest.stderr.on('data', (data) => {
118
+ console.log('Stdio stderr:', data.toString());
119
+ });
120
+
121
+ stdioTest.on('close', (code) => {
122
+ console.log('Stdio test exited with code:', code);
123
+ console.log('Stdio output:', stdioOutput);
124
+
125
+ if (stdioOutput.includes('"tools"') || stdioOutput.includes('MCP server is running')) {
126
+ console.log('โœ… Stdio Test PASSED: Stdio mode is working\n');
158
127
  } else {
159
- console.log('โŒ FAILED: No clear success indicators found');
160
- console.error(e);
128
+ console.log('โŒ Stdio Test FAILED: Stdio mode not working correctly\n');
161
129
  }
162
- }
130
+
131
+ console.log('=== All Tests Completed ===');
132
+ });
133
+
134
+ // Give it a moment then close stdin
135
+ setTimeout(() => {
136
+ stdioTest.stdin.end();
137
+ }, 1000);
163
138
  });
164
139
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vint.tri/report_gen_mcp",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "CLI tool for generating HTML reports with embedded charts",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@modelcontextprotocol/sdk": "^1.0.0",
16
+ "@vint.tri/report_gen_mcp": "^1.0.31",
16
17
  "chart.js": "^3.9.1",
17
18
  "chartjs-node-canvas": "^4.1.6",
18
19
  "commander": "^12.1.0",
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import os from 'os';
9
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
11
  import { z } from 'zod';
12
+ import { pathToFileURL } from 'url';
12
13
 
13
14
  // Check if we're running in stdio mode (no command-line arguments)
14
15
  const isStdioMode = process.argv.length === 2;
@@ -94,7 +95,7 @@ if (process.argv.length === 2) {
94
95
  // No command specified, run in stdio mode using MCP SDK
95
96
  const mcpServer = new McpServer({
96
97
  name: "report_gen_mcp",
97
- version: "1.0.29",
98
+ version: "1.0.31",
98
99
  }, {
99
100
  // Disable health check to prevent automatic calls
100
101
  capabilities: {
@@ -124,7 +125,7 @@ if (process.argv.length === 2) {
124
125
  }),
125
126
  })).describe("Chart configurations mapped by ID"),
126
127
  outputFile: z.string().optional().describe("Output HTML file path"),
127
- tempDirectory: z.string().describe("Temporary directory for file storage (required to prevent read-only file system errors)"),
128
+ tempDirectory: z.string().optional().describe("Temporary directory for file storage (optional, will use REPORTS_DIR environment variable if set)"),
128
129
  },
129
130
  }, async (params: any) => {
130
131
  // Handle case where arguments might be sent as a JSON string by Claude desktop
@@ -139,18 +140,33 @@ if (process.argv.length === 2) {
139
140
  processedParams = params.arguments;
140
141
  }
141
142
 
142
- // Check if tempDirectory parameter is provided
143
- if (!processedParams.tempDirectory) {
144
- throw new Error('tempDirectory parameter is required. Please provide the directory where reports should be generated.');
143
+ const { document, charts, outputFile = 'report.html', tempDirectory } = processedParams;
144
+
145
+ // Determine the output directory:
146
+ // 1. Use REPORTS_DIR environment variable if set
147
+ // 2. Fall back to tempDirectory parameter if provided
148
+ // 3. Default to system temp directory if neither is available
149
+ let outputDir: string;
150
+ if (process.env.REPORTS_DIR) {
151
+ outputDir = process.env.REPORTS_DIR;
152
+ // Ensure the reports directory exists
153
+ try {
154
+ fs.ensureDirSync(outputDir);
155
+ } catch (error) {
156
+ throw new Error(`Cannot create or access the reports directory: ${outputDir}`);
157
+ }
158
+ } else if (tempDirectory) {
159
+ outputDir = tempDirectory;
160
+ } else {
161
+ outputDir = os.tmpdir();
145
162
  }
146
163
 
147
- const { document, charts, outputFile = 'report.html', tempDirectory } = processedParams;
148
- const outputPath = path.resolve(tempDirectory, outputFile);
164
+ const outputPath = path.resolve(outputDir, outputFile);
149
165
 
150
166
  try {
151
167
  const result = await generateReport(document, charts, outputPath);
152
- // Properly encode the file path for URL
153
- const encodedPath = encodeURIComponent(outputPath).replace(/%2F/g, '/');
168
+ // Generate proper file URL
169
+ const fileUrl = pathToFileURL(outputPath).href;
154
170
  return {
155
171
  content: [
156
172
  {
@@ -159,7 +175,7 @@ if (process.argv.length === 2) {
159
175
  ...result,
160
176
  message: "Report generated successfully",
161
177
  filePath: outputPath,
162
- fileUrl: `file://${encodedPath}`
178
+ fileUrl: fileUrl
163
179
  })
164
180
  }
165
181
  ]
@@ -182,9 +198,8 @@ if (process.argv.length === 2) {
182
198
  // Check if file exists
183
199
  await fs.access(filePath);
184
200
 
185
- // Properly encode the file path for URL
186
- const encodedPath = encodeURIComponent(filePath).replace(/%2F/g, '/');
187
- const fileUrl = `file://${encodedPath}`;
201
+ // Generate proper file URL
202
+ const fileUrl = pathToFileURL(filePath).href;
188
203
  return {
189
204
  content: [
190
205
  {
@@ -0,0 +1,15 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>Report</title>
7
+ <style> body { font-family: Arial, sans-serif; } img { max-width: 100%; } </style>
8
+ </head>
9
+ <body>
10
+ <h1>Test Report</h1>
11
+ <p>This is a test report with a <img src="data:image/png;base64,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993PL6W8ZuFBTdM8vtb6jNnZ2Q9ExNqJiYkvllJeMBgM7px3t9URseYwz7/+gQwPHNPWjXoAYGSW2Pq/7cSItaMeAhbR5UtqzR3wQvOJiYl/e88997y11vqhwWDwroW311o/U0p5+fT09PsnJyd7N9988zsj4jci4jfn3W1XROw8xHOvO8ztwPLnNQCOX0to/a+9a9QTwOK69JaIy5bMmvuuAGnb9pKIuLDruv98sIvKB4PBe/b9Pjk5Ode27Z9FxIWLNyYAALAc7HcNSL/fPzciXhARTz7UJ1pNTEy8p9/vnzlv16Mj4pbFGREAAFgu9jsDUmv9tVrrR5umuaDf7+/bff309PR1/X7/j2qtKweDwaZSyvtqrdf0+/3/EhFraq0X11qfmz49AABwTJl/BqRExFubpvmnA92x67q/rbVeGxExGAymu647NyIeXmvdMxwOf3xmZubGhHkBAIBjWBnhc58REaIFjl/rY0ldhAokWmLrv14REa8a9RSwiDZElCWz5hZ+DwgAAMCiESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkESAAAEAaAQIAAKQRIAAAQBoBAgAApBEgAABAGgECAACkOWCAtG170sUXX/yQwx188cUXP+Scc845+eiPBQAALEdl/saFF164puu6t9VaHx0RJ5RSPr579+5f3LZt267599u4cePKNWvWvCMi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" alt="Chart" /> chart.</p>
12
+
13
+ </body>
14
+ </html>
15
+