grove-mcp 1.0.4 → 1.0.7

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 +167 -74
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
  const https = require('https');
19
19
  const readline = require('readline');
20
+ const crypto = require('crypto');
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
20
24
 
21
25
  const HOSTNAME = 'mcp.lyra.tg';
22
26
  const SSE_PATH = '/sse';
@@ -29,100 +33,189 @@ if (process.env.GROVE_OUTLINE_TOKEN)
29
33
  TOKEN_HEADERS['Grove-Outline-Token'] = process.env.GROVE_OUTLINE_TOKEN;
30
34
  if (process.env.GROVE_MATTERMOST_TOKEN)
31
35
  TOKEN_HEADERS['Grove-Mattermost-Token'] = process.env.GROVE_MATTERMOST_TOKEN;
36
+
32
37
  // Per-user session isolation: scopes Redis session keys so multiple users
33
38
  // on the shared 3-pod cluster never overwrite each other's auth tokens.
34
- // Generate your key once with: grove_auth_start(email=...) and copy the
35
- // returned session_key into your claude_desktop_config.json env block.
36
- if (process.env.GROVE_SESSION_KEY)
37
- TOKEN_HEADERS['X-Grove-Session-Key'] = process.env.GROVE_SESSION_KEY;
39
+ // If GROVE_SESSION_KEY is set in the environment and is not the placeholder
40
+ // value, use it directly. Otherwise auto-generate a stable random key the
41
+ // first time and persist it to ~/.grove-mcp/session-key so that it survives
42
+ // restarts without any manual configuration.
43
+ (function resolveSessionKey() {
44
+ const PLACEHOLDER = '<your-session-key-here>';
45
+ const envKey = process.env.GROVE_SESSION_KEY;
46
+
47
+ if (envKey && envKey !== PLACEHOLDER) {
48
+ TOKEN_HEADERS['X-Grove-Session-Key'] = envKey;
49
+ return;
50
+ }
51
+
52
+ const keyFile = path.join(os.homedir(), '.grove-mcp', 'session-key');
53
+ try {
54
+ const stored = fs.readFileSync(keyFile, 'utf8').trim();
55
+ if (stored) {
56
+ TOKEN_HEADERS['X-Grove-Session-Key'] = stored;
57
+ return;
58
+ }
59
+ } catch (_) {
60
+ // File doesn't exist yet — generate below.
61
+ }
62
+
63
+ const generated = crypto.randomBytes(16).toString('hex');
64
+ try {
65
+ fs.mkdirSync(path.dirname(keyFile), { recursive: true });
66
+ fs.writeFileSync(keyFile, generated, { mode: 0o600 });
67
+ } catch (err) {
68
+ process.stderr.write(
69
+ `grove-mcp-bridge: warning: could not persist session key to ${keyFile}: ${err.message}\n`
70
+ );
71
+ }
72
+ TOKEN_HEADERS['X-Grove-Session-Key'] = generated;
73
+ })();
74
+
75
+ // Strip MCP spec fields that older Claude Desktop clients don't handle.
76
+ // FastMCP 3.x auto-generates outputSchema from Python return type annotations;
77
+ // Claude Desktop silently drops ALL tools when it sees this field (claude-code#25081).
78
+ function stripMcpCompat(raw) {
79
+ try {
80
+ const msg = JSON.parse(raw);
81
+ if (msg.result && Array.isArray(msg.result.tools)) {
82
+ msg.result.tools = msg.result.tools.map(function (t) {
83
+ // eslint-disable-next-line no-unused-vars
84
+ var outputSchema = t.outputSchema, rest = Object.assign({}, t);
85
+ delete rest.outputSchema;
86
+ return rest;
87
+ });
88
+ }
89
+ return JSON.stringify(msg);
90
+ } catch (_) {
91
+ return raw;
92
+ }
93
+ }
38
94
 
39
95
  let sessionPath = null;
40
96
  let affinityCookie = null; // sticky-session cookie from SSE response headers
41
97
  const pending = [];
42
98
 
43
- // ── SSE connection ──────────────────────────────────────────────────────────
44
-
45
- const sseReq = https.request(
46
- {
47
- hostname: HOSTNAME,
48
- path: SSE_PATH,
49
- method: 'GET',
50
- headers: { Accept: 'text/event-stream' },
51
- },
52
- (res) => {
53
- if (res.statusCode !== 200) {
54
- process.stderr.write(
55
- `grove-mcp-bridge: SSE connect failed (HTTP ${res.statusCode})\n`
56
- );
57
- process.exit(1);
58
- }
99
+ // ── SSE connection with reconnect ───────────────────────────────────────────
59
100
 
60
- // Capture nginx sticky-session cookie so POSTs reach the same pod
61
- const setCookie = res.headers['set-cookie'];
62
- if (setCookie) {
63
- const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
64
- const parts = cookies.map((c) => c.split(';')[0].trim());
65
- affinityCookie = parts.join('; ');
66
- }
101
+ let reconnectDelay = 1000; // ms; doubles on each failure, capped at 30s
67
102
 
68
- let buf = '';
69
- let eventType = '';
103
+ function connect() {
104
+ // Reset session state so stdin messages are queued until the new endpoint arrives
105
+ sessionPath = null;
106
+ affinityCookie = null;
70
107
 
71
- res.on('data', (chunk) => {
72
- buf += chunk.toString('utf8');
108
+ const sseReq = https.request(
109
+ {
110
+ hostname: HOSTNAME,
111
+ path: SSE_PATH,
112
+ method: 'GET',
113
+ headers: { Accept: 'text/event-stream' },
114
+ },
115
+ (res) => {
116
+ if (res.statusCode !== 200) {
117
+ process.stderr.write(
118
+ `grove-mcp-bridge: SSE connect failed (HTTP ${res.statusCode}), retrying in ${reconnectDelay}ms\n`
119
+ );
120
+ res.on('error', () => {}); // prevent unhandled-error crash while draining
121
+ res.resume();
122
+ scheduleReconnect();
123
+ return;
124
+ }
73
125
 
74
- let nl;
75
- while ((nl = buf.indexOf('\n')) !== -1) {
76
- const line = buf.slice(0, nl).replace(/\r$/, '');
77
- buf = buf.slice(nl + 1);
126
+ // Successful connection — reset backoff
127
+ reconnectDelay = 1000;
78
128
 
79
- if (line.startsWith('event:')) {
80
- eventType = line.slice(6).trim();
81
- } else if (line.startsWith('data:')) {
82
- const raw = line.slice(5).trim();
129
+ // Capture nginx sticky-session cookie so POSTs reach the same pod
130
+ const setCookie = res.headers['set-cookie'];
131
+ if (setCookie) {
132
+ const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
133
+ const parts = cookies.map((c) => c.split(';')[0].trim());
134
+ affinityCookie = parts.join('; ');
135
+ }
83
136
 
84
- if (eventType === 'endpoint') {
85
- try {
86
- sessionPath = JSON.parse(raw).uri;
87
- } catch (_) {
88
- // Fallback: server sent a plain path instead of JSON
89
- sessionPath = raw;
137
+ let buf = '';
138
+ let eventType = '';
139
+
140
+ res.on('data', (chunk) => {
141
+ buf += chunk.toString('utf8');
142
+
143
+ let nl;
144
+ while ((nl = buf.indexOf('\n')) !== -1) {
145
+ const line = buf.slice(0, nl).replace(/\r$/, '');
146
+ buf = buf.slice(nl + 1);
147
+
148
+ if (line.startsWith('event:')) {
149
+ eventType = line.slice(6).trim();
150
+ } else if (line.startsWith('data:')) {
151
+ const raw = line.slice(5).trim();
152
+
153
+ if (eventType === 'endpoint') {
154
+ try {
155
+ sessionPath = JSON.parse(raw).uri;
156
+ } catch (_) {
157
+ // Fallback: server sent a plain path instead of JSON
158
+ sessionPath = raw;
159
+ }
160
+ // Flush any stdin lines that arrived before the endpoint was ready
161
+ for (const msg of pending) sendToServer(msg);
162
+ pending.length = 0;
163
+ } else if (eventType === 'message' && raw) {
164
+ // Strip modern MCP spec fields that cause Claude Desktop to silently
165
+ // drop all tools (anthropics/claude-code#25081): FastMCP 3.x emits
166
+ // outputSchema in tools/list which Claude Desktop doesn't understand.
167
+ process.stdout.write(stripMcpCompat(raw) + '\n');
90
168
  }
91
- // Flush any stdin lines that arrived before the endpoint was ready
92
- for (const msg of pending) sendToServer(msg);
93
- pending.length = 0;
94
- } else if (eventType === 'message' && raw) {
95
- // raw is already a serialised JSON-RPC object — write directly
96
- process.stdout.write(raw + '\n');
97
- }
98
169
 
99
- eventType = '';
100
- } else if (line === '') {
101
- eventType = '';
170
+ eventType = '';
171
+ } else if (line === '') {
172
+ eventType = '';
173
+ }
102
174
  }
103
- }
104
- });
175
+ });
105
176
 
106
- res.on('end', () => {
107
- process.stderr.write('grove-mcp-bridge: SSE stream ended\n');
108
- process.exit(0);
109
- });
177
+ res.on('end', () => {
178
+ process.stderr.write(`grove-mcp-bridge: SSE stream ended, reconnecting in ${reconnectDelay}ms\n`);
179
+ scheduleReconnect();
180
+ });
110
181
 
111
- res.on('error', (err) => {
112
- process.stderr.write(`grove-mcp-bridge: SSE error: ${err.message}\n`);
113
- process.exit(1);
114
- });
115
- }
116
- );
117
-
118
- sseReq.on('error', (err) => {
119
- process.stderr.write(
120
- `grove-mcp-bridge: cannot reach ${HOSTNAME}: ${err.message}\n`
182
+ res.on('error', (err) => {
183
+ process.stderr.write(`grove-mcp-bridge: SSE error: ${err.message}, reconnecting in ${reconnectDelay}ms\n`);
184
+ scheduleReconnect();
185
+ });
186
+ }
121
187
  );
122
- process.exit(1);
123
- });
124
188
 
125
- sseReq.end();
189
+ sseReq.on('error', (err) => {
190
+ process.stderr.write(
191
+ `grove-mcp-bridge: cannot reach ${HOSTNAME}: ${err.message}, retrying in ${reconnectDelay}ms\n`
192
+ );
193
+ scheduleReconnect();
194
+ });
195
+
196
+ // Server sends keepalive pings every 30s. If nothing arrives in 75s the
197
+ // connection is stale (server crash, network partition) — destroy and reconnect.
198
+ sseReq.setTimeout(75000);
199
+ sseReq.on('timeout', () => {
200
+ process.stderr.write(`grove-mcp-bridge: SSE idle timeout, reconnecting in ${reconnectDelay}ms\n`);
201
+ sseReq.destroy(); // triggers sseReq.on('error') or res.on('error') → scheduleReconnect()
202
+ });
203
+
204
+ sseReq.end();
205
+ }
206
+
207
+ function scheduleReconnect() {
208
+ // Null the session immediately so stdin messages are queued rather than
209
+ // fired at a dead endpoint during the reconnect delay window.
210
+ sessionPath = null;
211
+ affinityCookie = null;
212
+ setTimeout(() => {
213
+ connect();
214
+ }, reconnectDelay);
215
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
216
+ }
217
+
218
+ connect();
126
219
 
127
220
  // ── stdin → POST ────────────────────────────────────────────────────────────
128
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grove-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "Claude Desktop bridge for the Grove MCP Server (Mattermost + Outline)",
5
5
  "main": "index.js",
6
6
  "bin": {