reportdash-datastore-mcp-claude-desktop 1.0.9 → 1.0.11

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.
Files changed (2) hide show
  1. package/index.js +105 -68
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -12,9 +12,8 @@ const API_URL = process.env.REPORTDASH_API_URL || 'https://datastore.reportdash.
12
12
 
13
13
  // Validate configuration
14
14
  if (!API_KEY) {
15
- // NOTE: In MCP stdio mode, prefer stdout only.
16
- // But keeping your original behavior as-is.
17
- console.error(
15
+ // In Claude MCP mode, stdout should be JSON only. This is fatal anyway, so ok.
16
+ process.stdout.write(
18
17
  JSON.stringify({
19
18
  jsonrpc: '2.0',
20
19
  error: {
@@ -22,14 +21,14 @@ if (!API_KEY) {
22
21
  message:
23
22
  'REPORTDASH_API_KEY environment variable is required. Get your API key from ReportDash DataStore app settings.',
24
23
  },
25
- })
24
+ id: null,
25
+ }) + '\n'
26
26
  );
27
27
  process.exit(1);
28
28
  }
29
29
 
30
30
  /**
31
31
  * Ensure every outbound request includes platform="claude"
32
- * without breaking existing requests.
33
32
  */
34
33
  function withClaudePlatform(mcpRequest) {
35
34
  if (mcpRequest && typeof mcpRequest === 'object') {
@@ -39,14 +38,29 @@ function withClaudePlatform(mcpRequest) {
39
38
  }
40
39
 
41
40
  /**
42
- * JSON-RPC id helper:
43
- * Keep 0 as a valid id; only default when null/undefined.
41
+ * Preserve id=0. Only default when null/undefined.
44
42
  */
45
- function rpcId(mcpRequest) {
46
- return mcpRequest?.id ?? null;
43
+ function rpcId(req) {
44
+ return req?.id ?? null;
47
45
  }
48
46
 
49
- // Test mode
47
+ /**
48
+ * Write exactly one JSON object per line to stdout (MCP framing).
49
+ */
50
+ function writeJson(obj) {
51
+ process.stdout.write(JSON.stringify(obj) + '\n');
52
+ }
53
+
54
+ /**
55
+ * Build a JSON-RPC error response
56
+ */
57
+ function rpcError({ id = null, code = -32603, message = 'Internal error', data }) {
58
+ const err = { code, message };
59
+ if (data !== undefined) err.data = data;
60
+ return { jsonrpc: '2.0', id, error: err };
61
+ }
62
+
63
+ // Test mode (prints human logs to stdout; do not run in Claude MCP mode)
50
64
  if (process.argv.includes('--test')) {
51
65
  testConnection();
52
66
  return;
@@ -62,22 +76,21 @@ const rl = readline.createInterface({
62
76
  rl.on('line', (line) => {
63
77
  if (!line.trim()) return;
64
78
 
79
+ let mcpRequest;
65
80
  try {
66
- const mcpRequest = JSON.parse(line);
67
- forwardToAPI(withClaudePlatform(mcpRequest));
81
+ mcpRequest = JSON.parse(line);
68
82
  } catch (error) {
69
- // Only output to stdout, never stderr for MCP protocol
70
- console.log(
71
- JSON.stringify({
72
- jsonrpc: '2.0',
73
- error: {
74
- code: -32700,
75
- message: 'Parse error: ' + error.message,
76
- },
83
+ writeJson(
84
+ rpcError({
77
85
  id: null,
86
+ code: -32700,
87
+ message: 'Parse error: ' + error.message,
78
88
  })
79
89
  );
90
+ return;
80
91
  }
92
+
93
+ forwardToAPI(withClaudePlatform(mcpRequest));
81
94
  });
82
95
 
83
96
  function forwardToAPI(mcpRequest) {
@@ -85,96 +98,123 @@ function forwardToAPI(mcpRequest) {
85
98
  const isHttps = url.protocol === 'https:';
86
99
  const client = isHttps ? https : http;
87
100
 
101
+ const body = JSON.stringify(mcpRequest);
102
+
103
+ const hasId = mcpRequest && Object.prototype.hasOwnProperty.call(mcpRequest, 'id');
104
+ const reqId = hasId ? mcpRequest.id : undefined;
105
+
88
106
  const options = {
89
107
  hostname: url.hostname,
90
108
  port: url.port || (isHttps ? 443 : 80),
91
- path: url.pathname,
109
+ path: url.pathname + url.search,
92
110
  method: 'POST',
93
111
  headers: {
94
112
  'Content-Type': 'application/json',
113
+ 'Content-Length': Buffer.byteLength(body),
95
114
  'X-Api-Key': API_KEY,
96
115
  'User-Agent': 'ReportDash-DataStore-MCP/1.0',
97
116
  },
98
- timeout: 30000, // 30 second timeout
117
+ timeout: 30000,
99
118
  };
100
119
 
101
120
  const req = client.request(options, (res) => {
102
121
  let data = '';
103
-
104
- res.on('data', (chunk) => {
105
- data += chunk;
106
- });
107
-
122
+ res.on('data', (chunk) => (data += chunk));
108
123
  res.on('end', () => {
109
- // Treat 200-299 as success (including 204 No Content)
110
- if (res.statusCode >= 200 && res.statusCode < 300) {
111
- // For 204 No Content or empty response, return empty success
112
- if (res.statusCode === 204 || !data.trim()) {
113
- console.log(
124
+ // Notifications MUST NOT get responses
125
+ if (!hasId) return;
126
+
127
+ // 204 / empty => empty result
128
+ if (res.statusCode === 204 || !data.trim()) {
129
+ if (res.statusCode >= 200 && res.statusCode < 300) {
130
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: reqId, result: {} }) + '\n');
131
+ } else {
132
+ process.stdout.write(
114
133
  JSON.stringify({
115
134
  jsonrpc: '2.0',
116
- result: {},
117
- id: rpcId(mcpRequest), // FIX: preserves id=0
118
- })
135
+ id: reqId,
136
+ error: { code: res.statusCode, message: `API error: ${res.statusCode} (empty body)` },
137
+ }) + '\n'
119
138
  );
139
+ }
140
+ return;
141
+ }
142
+
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(data);
146
+ } catch (e) {
147
+ process.stdout.write(
148
+ JSON.stringify({
149
+ jsonrpc: '2.0',
150
+ id: reqId,
151
+ error: {
152
+ code: -32603,
153
+ message: 'API returned non-JSON response',
154
+ data: { statusCode: res.statusCode, body: data },
155
+ },
156
+ }) + '\n'
157
+ );
158
+ return;
159
+ }
160
+
161
+ // Success
162
+ if (res.statusCode >= 200 && res.statusCode < 300) {
163
+ // If backend returned a JSON-RPC object, enforce id
164
+ if (parsed && typeof parsed === 'object' && parsed.jsonrpc === '2.0') {
165
+ if (!Object.prototype.hasOwnProperty.call(parsed, 'id')) parsed.id = reqId;
166
+ // IMPORTANT: never allow null id
167
+ if (parsed.id === null) parsed.id = reqId;
168
+ process.stdout.write(JSON.stringify(parsed) + '\n');
120
169
  } else {
121
- // Forward the response as-is
122
- console.log(data);
170
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: reqId, result: parsed }) + '\n');
123
171
  }
124
172
  } else {
125
- // Send errors to stdout (not stderr!) so Claude can properly handle them
126
- console.log(
173
+ // Error
174
+ process.stdout.write(
127
175
  JSON.stringify({
128
176
  jsonrpc: '2.0',
177
+ id: reqId,
129
178
  error: {
130
179
  code: res.statusCode,
131
- message: `API error: ${res.statusCode}${data ? ' - ' + data : ''}`,
132
- data: { statusCode: res.statusCode, body: data },
180
+ message: `API error: ${res.statusCode}`,
181
+ data: parsed,
133
182
  },
134
- id: rpcId(mcpRequest), // FIX: preserves id=0
135
- })
183
+ }) + '\n'
136
184
  );
137
185
  }
138
186
  });
139
187
  });
140
188
 
141
189
  req.on('error', (error) => {
142
- // Send errors to stdout (not stderr!)
143
- console.log(
190
+ if (!hasId) return; // notification: no response
191
+ process.stdout.write(
144
192
  JSON.stringify({
145
193
  jsonrpc: '2.0',
146
- error: {
147
- code: -32603,
148
- message: 'Network error: ' + error.message,
149
- data: { error: error.message },
150
- },
151
- id: rpcId(mcpRequest), // FIX: preserves id=0
152
- })
194
+ id: reqId,
195
+ error: { code: -32603, message: 'Network error: ' + error.message },
196
+ }) + '\n'
153
197
  );
154
198
  });
155
199
 
156
200
  req.on('timeout', () => {
157
201
  req.destroy();
158
- // Send errors to stdout (not stderr!)
159
- console.log(
202
+ if (!hasId) return; // notification: no response
203
+ process.stdout.write(
160
204
  JSON.stringify({
161
205
  jsonrpc: '2.0',
162
- error: {
163
- code: -32603,
164
- message: 'Request timeout after 30 seconds',
165
- },
166
- id: rpcId(mcpRequest), // FIX: preserves id=0
167
- })
206
+ id: reqId,
207
+ error: { code: -32603, message: 'Request timeout after 30 seconds' },
208
+ }) + '\n'
168
209
  );
169
210
  });
170
211
 
171
- req.write(JSON.stringify(mcpRequest));
212
+ req.write(body);
172
213
  req.end();
173
214
  }
174
215
 
216
+
175
217
  function testConnection() {
176
- // NOTE: This prints non-JSON to stdout, which is fine for --test.
177
- // Do not run --test as an MCP stdio server in Claude Desktop.
178
218
  console.log('🔍 Testing ReportDash DataStore connection...\n');
179
219
  console.log(`API URL: ${API_URL}`);
180
220
  console.log(
@@ -185,7 +225,6 @@ function testConnection() {
185
225
  const isHttps = url.protocol === 'https:';
186
226
  const client = isHttps ? https : http;
187
227
 
188
- // Create MCP tools/list request
189
228
  const mcpRequest = withClaudePlatform({
190
229
  jsonrpc: '2.0',
191
230
  method: 'tools/list',
@@ -197,7 +236,7 @@ function testConnection() {
197
236
  const options = {
198
237
  hostname: url.hostname,
199
238
  port: url.port || (isHttps ? 443 : 80),
200
- path: url.pathname,
239
+ path: url.pathname + url.search,
201
240
  method: 'POST',
202
241
  headers: {
203
242
  'Content-Type': 'application/json',
@@ -212,9 +251,7 @@ function testConnection() {
212
251
 
213
252
  const req = client.request(options, (res) => {
214
253
  let data = '';
215
- res.on('data', (chunk) => {
216
- data += chunk;
217
- });
254
+ res.on('data', (chunk) => (data += chunk));
218
255
  res.on('end', () => {
219
256
  console.log(`Response Status: ${res.statusCode}\n`);
220
257
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reportdash-datastore-mcp-claude-desktop",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "ReportDash DataStore MCP server for Claude Desktop",
5
5
  "main": "index.js",
6
6
  "bin": {