groove-dev 0.17.0 → 0.17.2

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.
@@ -11,10 +11,20 @@
11
11
  "transport": "stdio",
12
12
  "command": "npx",
13
13
  "args": ["-y", "@modelcontextprotocol/server-slack"],
14
+ "authType": "api-key",
14
15
  "envKeys": [
15
16
  { "key": "SLACK_BOT_TOKEN", "label": "Bot Token", "placeholder": "xoxb-...", "required": true },
16
17
  { "key": "SLACK_TEAM_ID", "label": "Team ID", "placeholder": "T01234567", "required": true }
17
18
  ],
19
+ "setupUrl": "https://api.slack.com/apps",
20
+ "setupSteps": [
21
+ "Go to api.slack.com/apps and click 'Create New App'",
22
+ "Choose 'From scratch', name it 'Groove', pick your workspace",
23
+ "Go to 'OAuth & Permissions', add scopes: channels:read, channels:history, chat:write, users:read",
24
+ "Click 'Install to Workspace' and authorize",
25
+ "Copy the 'Bot User OAuth Token' (starts with xoxb-)",
26
+ "Your Team ID is in the workspace URL: app.slack.com/client/TXXXXXXX"
27
+ ],
18
28
  "featured": true,
19
29
  "downloads": 0,
20
30
  "rating": 0,
@@ -33,9 +43,16 @@
33
43
  "transport": "stdio",
34
44
  "command": "npx",
35
45
  "args": ["-y", "@modelcontextprotocol/server-github"],
46
+ "authType": "api-key",
36
47
  "envKeys": [
37
48
  { "key": "GITHUB_PERSONAL_ACCESS_TOKEN", "label": "Personal Access Token", "placeholder": "ghp_...", "required": true }
38
49
  ],
50
+ "setupUrl": "https://github.com/settings/tokens/new",
51
+ "setupSteps": [
52
+ "Click the link below to create a new token on GitHub",
53
+ "Select scopes: repo, read:org, read:user",
54
+ "Click 'Generate token' and copy it"
55
+ ],
39
56
  "featured": true,
40
57
  "downloads": 0,
41
58
  "rating": 0,
@@ -54,9 +71,16 @@
54
71
  "transport": "stdio",
55
72
  "command": "npx",
56
73
  "args": ["-y", "@stripe/agent-toolkit", "mcp"],
74
+ "authType": "api-key",
57
75
  "envKeys": [
58
76
  { "key": "STRIPE_SECRET_KEY", "label": "Secret Key", "placeholder": "sk_...", "required": true }
59
77
  ],
78
+ "setupUrl": "https://dashboard.stripe.com/apikeys",
79
+ "setupSteps": [
80
+ "Click the link below to open your Stripe API keys",
81
+ "Copy the 'Secret key' (starts with sk_live_ or sk_test_)",
82
+ "Use a test key (sk_test_) for safe experimentation"
83
+ ],
60
84
  "featured": true,
61
85
  "downloads": 0,
62
86
  "rating": 0,
@@ -75,10 +99,17 @@
75
99
  "transport": "stdio",
76
100
  "command": "npx",
77
101
  "args": ["-y", "@anthropic-ai/mcp-server-google-calendar"],
102
+ "authType": "oauth-google",
103
+ "oauthScopes": ["https://www.googleapis.com/auth/calendar"],
78
104
  "envKeys": [
79
- { "key": "GOOGLE_CLIENT_ID", "label": "OAuth Client ID", "required": true },
80
- { "key": "GOOGLE_CLIENT_SECRET", "label": "OAuth Client Secret", "required": true },
81
- { "key": "GOOGLE_REFRESH_TOKEN", "label": "Refresh Token", "required": true }
105
+ { "key": "GOOGLE_CLIENT_ID", "label": "OAuth Client ID", "required": true, "hidden": true },
106
+ { "key": "GOOGLE_CLIENT_SECRET", "label": "OAuth Client Secret", "required": true, "hidden": true },
107
+ { "key": "GOOGLE_REFRESH_TOKEN", "label": "Refresh Token", "required": true, "hidden": true }
108
+ ],
109
+ "setupSteps": [
110
+ "Click 'Connect with Google' below",
111
+ "Sign in and authorize Groove to access your calendar",
112
+ "That's it — your credentials are stored securely"
82
113
  ],
83
114
  "featured": false,
84
115
  "downloads": 0,
@@ -98,10 +129,17 @@
98
129
  "transport": "stdio",
99
130
  "command": "npx",
100
131
  "args": ["-y", "@anthropic-ai/mcp-server-gmail"],
132
+ "authType": "oauth-google",
133
+ "oauthScopes": ["https://www.googleapis.com/auth/gmail.modify"],
101
134
  "envKeys": [
102
- { "key": "GOOGLE_CLIENT_ID", "label": "OAuth Client ID", "required": true },
103
- { "key": "GOOGLE_CLIENT_SECRET", "label": "OAuth Client Secret", "required": true },
104
- { "key": "GOOGLE_REFRESH_TOKEN", "label": "Refresh Token", "required": true }
135
+ { "key": "GOOGLE_CLIENT_ID", "label": "OAuth Client ID", "required": true, "hidden": true },
136
+ { "key": "GOOGLE_CLIENT_SECRET", "label": "OAuth Client Secret", "required": true, "hidden": true },
137
+ { "key": "GOOGLE_REFRESH_TOKEN", "label": "Refresh Token", "required": true, "hidden": true }
138
+ ],
139
+ "setupSteps": [
140
+ "Click 'Connect with Google' below",
141
+ "Sign in and authorize Groove to access your email",
142
+ "That's it — your credentials are stored securely"
105
143
  ],
106
144
  "featured": false,
107
145
  "downloads": 0,
@@ -121,9 +159,14 @@
121
159
  "transport": "stdio",
122
160
  "command": "npx",
123
161
  "args": ["-y", "@modelcontextprotocol/server-postgres"],
162
+ "authType": "api-key",
124
163
  "envKeys": [
125
164
  { "key": "POSTGRES_CONNECTION_STRING", "label": "Connection String", "placeholder": "postgresql://user:pass@host:5432/db", "required": true }
126
165
  ],
166
+ "setupSteps": [
167
+ "Enter your PostgreSQL connection string",
168
+ "Format: postgresql://username:password@host:port/database"
169
+ ],
127
170
  "featured": false,
128
171
  "downloads": 0,
129
172
  "rating": 0,
@@ -142,9 +185,16 @@
142
185
  "transport": "stdio",
143
186
  "command": "npx",
144
187
  "args": ["-y", "@modelcontextprotocol/server-brave-search"],
188
+ "authType": "api-key",
145
189
  "envKeys": [
146
190
  { "key": "BRAVE_API_KEY", "label": "API Key", "placeholder": "BSA...", "required": true }
147
191
  ],
192
+ "setupUrl": "https://brave.com/search/api/",
193
+ "setupSteps": [
194
+ "Click the link below to get a Brave Search API key",
195
+ "Sign up for a free plan (2,000 queries/month)",
196
+ "Copy your API key"
197
+ ],
148
198
  "featured": false,
149
199
  "downloads": 0,
150
200
  "rating": 0,
@@ -163,10 +213,17 @@
163
213
  "transport": "stdio",
164
214
  "command": "npx",
165
215
  "args": ["-y", "@modelcontextprotocol/server-gdrive"],
216
+ "authType": "oauth-google",
217
+ "oauthScopes": ["https://www.googleapis.com/auth/drive.readonly"],
166
218
  "envKeys": [
167
- { "key": "GOOGLE_CLIENT_ID", "label": "OAuth Client ID", "required": true },
168
- { "key": "GOOGLE_CLIENT_SECRET", "label": "OAuth Client Secret", "required": true },
169
- { "key": "GOOGLE_REFRESH_TOKEN", "label": "Refresh Token", "required": true }
219
+ { "key": "GOOGLE_CLIENT_ID", "label": "OAuth Client ID", "required": true, "hidden": true },
220
+ { "key": "GOOGLE_CLIENT_SECRET", "label": "OAuth Client Secret", "required": true, "hidden": true },
221
+ { "key": "GOOGLE_REFRESH_TOKEN", "label": "Refresh Token", "required": true, "hidden": true }
222
+ ],
223
+ "setupSteps": [
224
+ "Click 'Connect with Google' below",
225
+ "Sign in and authorize Groove to access your Drive",
226
+ "That's it — your credentials are stored securely"
170
227
  ],
171
228
  "featured": false,
172
229
  "downloads": 0,
@@ -186,9 +243,16 @@
186
243
  "transport": "stdio",
187
244
  "command": "npx",
188
245
  "args": ["-y", "@ibraheem4/linear-mcp-server"],
246
+ "authType": "api-key",
189
247
  "envKeys": [
190
248
  { "key": "LINEAR_API_KEY", "label": "API Key", "required": true }
191
249
  ],
250
+ "setupUrl": "https://linear.app/settings/api",
251
+ "setupSteps": [
252
+ "Click the link below to open Linear API settings",
253
+ "Click 'Create key', give it a label like 'Groove'",
254
+ "Copy the API key"
255
+ ],
192
256
  "featured": false,
193
257
  "downloads": 0,
194
258
  "rating": 0,
@@ -207,9 +271,17 @@
207
271
  "transport": "stdio",
208
272
  "command": "npx",
209
273
  "args": ["-y", "@notionhq/notion-mcp-server"],
274
+ "authType": "api-key",
210
275
  "envKeys": [
211
276
  { "key": "NOTION_API_KEY", "label": "Integration Token", "placeholder": "ntn_...", "required": true }
212
277
  ],
278
+ "setupUrl": "https://www.notion.so/my-integrations",
279
+ "setupSteps": [
280
+ "Click the link below to open Notion integrations",
281
+ "Click 'New integration', name it 'Groove'",
282
+ "Copy the 'Internal Integration Secret'",
283
+ "Share the pages/databases you want accessible with the integration"
284
+ ],
213
285
  "featured": false,
214
286
  "downloads": 0,
215
287
  "rating": 0,
@@ -228,9 +300,18 @@
228
300
  "transport": "stdio",
229
301
  "command": "npx",
230
302
  "args": ["-y", "mcp-discord"],
303
+ "authType": "api-key",
231
304
  "envKeys": [
232
305
  { "key": "DISCORD_BOT_TOKEN", "label": "Bot Token", "required": true }
233
306
  ],
307
+ "setupUrl": "https://discord.com/developers/applications",
308
+ "setupSteps": [
309
+ "Click the link below to open Discord Developer Portal",
310
+ "Click 'New Application', name it 'Groove'",
311
+ "Go to 'Bot' tab, click 'Reset Token', copy it",
312
+ "Enable 'Message Content Intent' under Privileged Gateway Intents",
313
+ "Go to OAuth2 > URL Generator, select 'bot' scope, invite to your server"
314
+ ],
234
315
  "featured": false,
235
316
  "downloads": 0,
236
317
  "rating": 0,
@@ -249,10 +330,16 @@
249
330
  "transport": "stdio",
250
331
  "command": "npx",
251
332
  "args": ["-y", "mcp-home-assistant"],
333
+ "authType": "api-key",
252
334
  "envKeys": [
253
335
  { "key": "HOME_ASSISTANT_URL", "label": "HA URL", "placeholder": "http://homeassistant.local:8123", "required": true },
254
336
  { "key": "HOME_ASSISTANT_TOKEN", "label": "Long-Lived Access Token", "required": true }
255
337
  ],
338
+ "setupSteps": [
339
+ "Enter your Home Assistant URL (usually http://homeassistant.local:8123)",
340
+ "In HA: Profile (bottom left) > Security > Long-Lived Access Tokens",
341
+ "Click 'Create Token', name it 'Groove', copy the token"
342
+ ],
256
343
  "featured": false,
257
344
  "downloads": 0,
258
345
  "rating": 0,
@@ -271,6 +358,7 @@
271
358
  "transport": "stdio",
272
359
  "command": "npx",
273
360
  "args": ["-y", "@modelcontextprotocol/server-filesystem"],
361
+ "authType": "none",
274
362
  "envKeys": [],
275
363
  "featured": false,
276
364
  "downloads": 0,
@@ -290,9 +378,16 @@
290
378
  "transport": "stdio",
291
379
  "command": "npx",
292
380
  "args": ["-y", "@modelcontextprotocol/server-google-maps"],
381
+ "authType": "api-key",
293
382
  "envKeys": [
294
383
  { "key": "GOOGLE_MAPS_API_KEY", "label": "API Key", "required": true }
295
384
  ],
385
+ "setupUrl": "https://console.cloud.google.com/apis/credentials",
386
+ "setupSteps": [
387
+ "Click the link below to open Google Cloud Console",
388
+ "Create or select a project, then 'Create Credentials' > 'API Key'",
389
+ "Enable the Maps JavaScript API, Geocoding API, and Places API"
390
+ ],
296
391
  "featured": false,
297
392
  "downloads": 0,
298
393
  "rating": 0,
@@ -311,6 +406,7 @@
311
406
  "transport": "stdio",
312
407
  "command": "npx",
313
408
  "args": ["-y", "@modelcontextprotocol/server-sqlite"],
409
+ "authType": "none",
314
410
  "envKeys": [],
315
411
  "featured": false,
316
412
  "downloads": 0,
@@ -505,6 +505,58 @@ export function createApi(app, daemon) {
505
505
  }
506
506
  });
507
507
 
508
+ // --- Google OAuth flow ---
509
+
510
+ app.get('/api/integrations/google-oauth/status', (req, res) => {
511
+ res.json({ configured: daemon.integrations.isGoogleOAuthConfigured() });
512
+ });
513
+
514
+ app.post('/api/integrations/google-oauth/setup', (req, res) => {
515
+ try {
516
+ const { clientId, clientSecret } = req.body || {};
517
+ if (!clientId || !clientSecret) return res.status(400).json({ error: 'clientId and clientSecret are required' });
518
+ daemon.integrations.setCredential('google-oauth', 'GOOGLE_CLIENT_ID', clientId);
519
+ daemon.integrations.setCredential('google-oauth', 'GOOGLE_CLIENT_SECRET', clientSecret);
520
+ res.json({ ok: true });
521
+ } catch (err) {
522
+ res.status(400).json({ error: err.message });
523
+ }
524
+ });
525
+
526
+ app.post('/api/integrations/:id/oauth/start', (req, res) => {
527
+ try {
528
+ const url = daemon.integrations.getOAuthUrl(req.params.id);
529
+ res.json({ url });
530
+ } catch (err) {
531
+ res.status(400).json({ error: err.message });
532
+ }
533
+ });
534
+
535
+ app.get('/api/integrations/oauth/callback', async (req, res) => {
536
+ try {
537
+ const { code, state } = req.query;
538
+ if (!code || !state) return res.status(400).send('Missing code or state parameter');
539
+ await daemon.integrations.handleOAuthCallback(code, state);
540
+ // Return a nice HTML page that auto-closes
541
+ res.send(`<!DOCTYPE html><html><body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#1e2127;color:#e6e6e6">
542
+ <div style="text-align:center">
543
+ <div style="font-size:48px;margin-bottom:16px">&#10003;</div>
544
+ <h2>Connected!</h2>
545
+ <p style="color:#7a8394">You can close this tab and return to Groove.</p>
546
+ <script>setTimeout(()=>window.close(),2000)</script>
547
+ </div>
548
+ </body></html>`);
549
+ } catch (err) {
550
+ res.status(400).send(`<!DOCTYPE html><html><body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#1e2127;color:#e06c75">
551
+ <div style="text-align:center">
552
+ <h2>Connection Failed</h2>
553
+ <p>${err.message}</p>
554
+ <p style="color:#7a8394">Close this tab and try again in Groove.</p>
555
+ </div>
556
+ </body></html>`);
557
+ }
558
+ });
559
+
508
560
  // --- Agent Integrations (attach/detach) ---
509
561
 
510
562
  app.post('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
@@ -235,6 +235,8 @@ export class IntegrationStore {
235
235
  /**
236
236
  * Build MCP config object for a set of integration IDs.
237
237
  * Returns the mcpServers object to merge into .mcp.json.
238
+ * SECURITY: credentials are NOT included in the config file.
239
+ * They are injected at spawn time via process environment only.
238
240
  */
239
241
  buildMcpConfig(integrationIds) {
240
242
  const mcpServers = {};
@@ -244,23 +246,33 @@ export class IntegrationStore {
244
246
  if (!entry) continue;
245
247
  if (!this._isInstalled(id)) continue;
246
248
 
247
- // Build environment with credentials
248
- const env = {};
249
- for (const ek of (entry.envKeys || [])) {
250
- const val = this.getCredential(id, ek.key);
251
- if (val) env[ek.key] = val;
252
- }
253
-
249
+ // No env block credentials stay out of .mcp.json
254
250
  mcpServers[`groove-${id}`] = {
255
251
  command: entry.command || 'npx',
256
252
  args: entry.args || ['-y', entry.npmPackage],
257
- env,
258
253
  };
259
254
  }
260
255
 
261
256
  return mcpServers;
262
257
  }
263
258
 
259
+ /**
260
+ * Get environment variables with decrypted credentials for a set of integration IDs.
261
+ * These are passed to the agent process at spawn time (in-memory only, never written to disk).
262
+ */
263
+ getSpawnEnv(integrationIds) {
264
+ const env = {};
265
+ for (const id of integrationIds) {
266
+ const entry = this.registry.find((s) => s.id === id);
267
+ if (!entry) continue;
268
+ for (const ek of (entry.envKeys || [])) {
269
+ const val = this.getCredential(id, ek.key);
270
+ if (val) env[ek.key] = val;
271
+ }
272
+ }
273
+ return env;
274
+ }
275
+
264
276
  /**
265
277
  * Write/merge MCP config into the project root .mcp.json.
266
278
  * Only adds/updates groove-* entries, preserves user's own MCP configs.
@@ -359,6 +371,92 @@ export class IntegrationStore {
359
371
  }
360
372
  }
361
373
 
374
+ /**
375
+ * Start an OAuth flow for a Google integration.
376
+ * Returns the authorization URL to open in a browser.
377
+ */
378
+ getOAuthUrl(integrationId) {
379
+ const entry = this.registry.find((s) => s.id === integrationId);
380
+ if (!entry) throw new Error(`Integration not found: ${integrationId}`);
381
+ if (entry.authType !== 'oauth-google') throw new Error('Integration does not use OAuth');
382
+
383
+ // Check if user has provided their own Google OAuth client (stored globally)
384
+ const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
385
+ const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
386
+ if (!clientId || !clientSecret) {
387
+ throw new Error('Google OAuth not configured. Set up your Google Cloud project first.');
388
+ }
389
+
390
+ const port = this.daemon.port || 31415;
391
+ const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
392
+ const scopes = entry.oauthScopes || [];
393
+
394
+ const params = new URLSearchParams({
395
+ client_id: clientId,
396
+ redirect_uri: redirectUri,
397
+ response_type: 'code',
398
+ scope: scopes.join(' '),
399
+ access_type: 'offline',
400
+ prompt: 'consent',
401
+ state: integrationId,
402
+ });
403
+
404
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
405
+ }
406
+
407
+ /**
408
+ * Handle OAuth callback — exchange code for tokens.
409
+ */
410
+ async handleOAuthCallback(code, integrationId) {
411
+ const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
412
+ const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
413
+ if (!clientId || !clientSecret) {
414
+ throw new Error('Google OAuth credentials not found');
415
+ }
416
+
417
+ const port = this.daemon.port || 31415;
418
+ const redirectUri = `http://localhost:${port}/api/integrations/oauth/callback`;
419
+
420
+ const res = await fetch('https://oauth2.googleapis.com/token', {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
423
+ body: new URLSearchParams({
424
+ code,
425
+ client_id: clientId,
426
+ client_secret: clientSecret,
427
+ redirect_uri: redirectUri,
428
+ grant_type: 'authorization_code',
429
+ }),
430
+ });
431
+
432
+ if (!res.ok) {
433
+ const err = await res.json().catch(() => ({}));
434
+ throw new Error(`OAuth token exchange failed: ${err.error_description || err.error || 'unknown'}`);
435
+ }
436
+
437
+ const tokens = await res.json();
438
+
439
+ // Store the tokens for this integration
440
+ this.setCredential(integrationId, 'GOOGLE_CLIENT_ID', clientId);
441
+ this.setCredential(integrationId, 'GOOGLE_CLIENT_SECRET', clientSecret);
442
+ if (tokens.refresh_token) {
443
+ this.setCredential(integrationId, 'GOOGLE_REFRESH_TOKEN', tokens.refresh_token);
444
+ }
445
+
446
+ this.daemon.audit.log('integration.oauth.complete', { id: integrationId });
447
+
448
+ return { ok: true, integrationId };
449
+ }
450
+
451
+ /**
452
+ * Check if Google OAuth is configured (user has set up their Cloud project).
453
+ */
454
+ isGoogleOAuthConfigured() {
455
+ const clientId = this.getCredential('google-oauth', 'GOOGLE_CLIENT_ID');
456
+ const clientSecret = this.getCredential('google-oauth', 'GOOGLE_CLIENT_SECRET');
457
+ return !!(clientId && clientSecret);
458
+ }
459
+
362
460
  // --- Internal ---
363
461
 
364
462
  _isInstalled(integrationId) {
@@ -201,9 +201,12 @@ For normal file edits within your scope, proceed without review.
201
201
  }
202
202
  }
203
203
 
204
- // Write MCP config for agent integrations
204
+ // Write MCP config for agent integrations (command/args only, no secrets)
205
+ // Credentials are injected via process environment below
206
+ let integrationEnv = {};
205
207
  if (config.integrations?.length > 0 && this.daemon.integrations) {
206
208
  this.daemon.integrations.writeMcpJson(config.integrations);
209
+ integrationEnv = this.daemon.integrations.getSpawnEnv(config.integrations);
207
210
  }
208
211
 
209
212
  const { command, args, env } = provider.buildSpawnCommand(spawnConfig);
@@ -232,7 +235,7 @@ For normal file edits within your scope, proceed without review.
232
235
  // Spawn the process
233
236
  const proc = cpSpawn(command, args, {
234
237
  cwd: agent.workingDir || this.daemon.projectDir,
235
- env: { ...process.env, ...env, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name },
238
+ env: { ...process.env, ...env, ...integrationEnv, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name },
236
239
  stdio: ['ignore', 'pipe', 'pipe'],
237
240
  // Don't let agent process prevent daemon from exiting
238
241
  detached: false,