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.
- package/index.js +105 -68
- 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
|
-
//
|
|
16
|
-
|
|
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
|
-
*
|
|
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(
|
|
46
|
-
return
|
|
43
|
+
function rpcId(req) {
|
|
44
|
+
return req?.id ?? null;
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
|
|
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
|
-
|
|
67
|
-
forwardToAPI(withClaudePlatform(mcpRequest));
|
|
81
|
+
mcpRequest = JSON.parse(line);
|
|
68
82
|
} catch (error) {
|
|
69
|
-
|
|
70
|
-
|
|
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,
|
|
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
|
-
//
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
console.log(data);
|
|
170
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: reqId, result: parsed }) + '\n');
|
|
123
171
|
}
|
|
124
172
|
} else {
|
|
125
|
-
//
|
|
126
|
-
|
|
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}
|
|
132
|
-
data:
|
|
180
|
+
message: `API error: ${res.statusCode}`,
|
|
181
|
+
data: parsed,
|
|
133
182
|
},
|
|
134
|
-
|
|
135
|
-
})
|
|
183
|
+
}) + '\n'
|
|
136
184
|
);
|
|
137
185
|
}
|
|
138
186
|
});
|
|
139
187
|
});
|
|
140
188
|
|
|
141
189
|
req.on('error', (error) => {
|
|
142
|
-
|
|
143
|
-
|
|
190
|
+
if (!hasId) return; // notification: no response
|
|
191
|
+
process.stdout.write(
|
|
144
192
|
JSON.stringify({
|
|
145
193
|
jsonrpc: '2.0',
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
159
|
-
|
|
202
|
+
if (!hasId) return; // notification: no response
|
|
203
|
+
process.stdout.write(
|
|
160
204
|
JSON.stringify({
|
|
161
205
|
jsonrpc: '2.0',
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
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
|
|