@assistkick/create 1.21.0 → 1.22.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistkick/create",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Scaffold assistkick-product-system into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * MCP config routes — per-user MCP server configuration management.
3
- * GET /api/mcp-config — get all configs for the authenticated user
4
- * PUT /api/mcp-config/:type — upsert a config (type = localhost | remote)
5
- * DELETE /api/mcp-config/:type — delete a config
3
+ * All endpoints accept an optional ?projectId query param to scope configs to a project.
4
+ * Without projectId, configs are global (user-level).
5
+ *
6
+ * GET /api/mcp-config?projectId=... — get configs for the authenticated user
7
+ * PUT /api/mcp-config/:type?projectId=... — upsert a config (type = localhost | remote)
8
+ * DELETE /api/mcp-config/:type?projectId=... — delete a config
6
9
  */
7
10
 
8
11
  import { Router } from 'express';
@@ -16,16 +19,15 @@ interface McpConfigRoutesDeps {
16
19
  export const createMcpConfigRoutes = ({ mcpConfigService, log }: McpConfigRoutesDeps): Router => {
17
20
  const router: Router = Router();
18
21
 
19
- // GET /api/mcp-config — list all configs for the authenticated user
22
+ // GET /api/mcp-config?projectId=...
20
23
  router.get('/', async (req, res) => {
21
24
  const userId = (req as any).user?.id;
22
- if (!userId) {
23
- res.status(401).json({ error: 'Unauthorized' });
24
- return;
25
- }
25
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
26
+
27
+ const projectId = (req.query.projectId as string) || null;
26
28
 
27
29
  try {
28
- const configs = await mcpConfigService.getAllConfigs(userId);
30
+ const configs = await mcpConfigService.getAllConfigs(userId, projectId);
29
31
  res.json({ configs });
30
32
  } catch (err: any) {
31
33
  log('MCP_CONFIG', `List configs failed: ${err.message}`);
@@ -33,15 +35,13 @@ export const createMcpConfigRoutes = ({ mcpConfigService, log }: McpConfigRoutes
33
35
  }
34
36
  });
35
37
 
36
- // PUT /api/mcp-config/:type — upsert a config
38
+ // PUT /api/mcp-config/:type?projectId=...
37
39
  router.put('/:type', async (req, res) => {
38
40
  const userId = (req as any).user?.id;
39
- if (!userId) {
40
- res.status(401).json({ error: 'Unauthorized' });
41
- return;
42
- }
41
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
43
42
 
44
43
  const configType = req.params.type;
44
+ const projectId = (req.query.projectId as string) || (req.body.projectId as string) || null;
45
45
  const { mcpServersJson } = req.body;
46
46
 
47
47
  if (!mcpServersJson || typeof mcpServersJson !== 'string') {
@@ -50,7 +50,7 @@ export const createMcpConfigRoutes = ({ mcpConfigService, log }: McpConfigRoutes
50
50
  }
51
51
 
52
52
  try {
53
- const config = await mcpConfigService.upsertConfig(userId, configType, mcpServersJson);
53
+ const config = await mcpConfigService.upsertConfig(userId, configType, mcpServersJson, projectId);
54
54
  res.json({ config });
55
55
  } catch (err: any) {
56
56
  log('MCP_CONFIG', `Upsert config failed: ${err.message}`);
@@ -62,18 +62,16 @@ export const createMcpConfigRoutes = ({ mcpConfigService, log }: McpConfigRoutes
62
62
  }
63
63
  });
64
64
 
65
- // DELETE /api/mcp-config/:type — delete a config
65
+ // DELETE /api/mcp-config/:type?projectId=...
66
66
  router.delete('/:type', async (req, res) => {
67
67
  const userId = (req as any).user?.id;
68
- if (!userId) {
69
- res.status(401).json({ error: 'Unauthorized' });
70
- return;
71
- }
68
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
72
69
 
73
70
  const configType = req.params.type;
71
+ const projectId = (req.query.projectId as string) || null;
74
72
 
75
73
  try {
76
- const deleted = await mcpConfigService.deleteConfig(userId, configType);
74
+ const deleted = await mcpConfigService.deleteConfig(userId, configType, projectId);
77
75
  if (!deleted) {
78
76
  res.status(404).json({ error: 'Config not found' });
79
77
  return;
@@ -430,7 +430,7 @@ export class ChatWsHandler {
430
430
  const userId = this.wsUserIds.get(ws);
431
431
  if (userId && this.mcpConfigService) {
432
432
  try {
433
- const mergedServers = await this.mcpConfigService.buildMergedConfig(userId);
433
+ const mergedServers = await this.mcpConfigService.buildMergedConfig(userId, projectId);
434
434
  if (mergedServers) {
435
435
  mcpConfigJson = JSON.stringify({ mcpServers: mergedServers });
436
436
  this.log('CHAT_WS', `MCP config: ${Object.keys(mergedServers).length} server(s) for user ${userId}`);
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * McpConfigService — CRUD operations for per-user MCP server configurations.
3
- * Each user has two config slots: 'localhost' (for local dev) and 'remote' (for Docker/cloud).
3
+ * Each user has two config slots per scope: 'localhost' (local dev) and 'remote' (Docker/cloud).
4
+ * Configs can be global (projectId=null) or project-scoped (projectId set).
4
5
  * Uses deterministic IDs to enforce uniqueness at the PK level.
5
6
  */
6
7
 
7
- import {eq} from 'drizzle-orm';
8
+ import {eq, and, isNull, or} from 'drizzle-orm';
8
9
  import {userMcpConfigs} from '@assistkick/shared/db/schema.js';
10
+ import {existsSync} from 'node:fs';
9
11
 
10
12
  interface McpConfigServiceDeps {
11
13
  getDb: () => any;
@@ -15,6 +17,7 @@ interface McpConfigServiceDeps {
15
17
  export interface McpConfigRecord {
16
18
  id: string;
17
19
  userId: string;
20
+ projectId: string | null;
18
21
  configType: string;
19
22
  mcpServersJson: string;
20
23
  createdAt: string;
@@ -25,33 +28,38 @@ export class McpConfigService {
25
28
  private readonly getDb: () => any;
26
29
  private readonly log: McpConfigServiceDeps['log'];
27
30
 
28
- constructor({ getDb, log }: McpConfigServiceDeps) {
31
+ constructor({getDb, log}: McpConfigServiceDeps) {
29
32
  this.getDb = getDb;
30
33
  this.log = log;
31
34
  }
32
35
 
33
- /** Generate a deterministic ID to enforce (userId, configType) uniqueness. */
34
- private makeId = (userId: string, configType: string): string => {
35
- return `mcpcfg_${userId}_${configType}`;
36
+ /** Generate a deterministic ID to enforce (userId, projectId, configType) uniqueness. */
37
+ private makeId = (userId: string, configType: string, projectId?: string | null): string => {
38
+ const scope = projectId || 'global';
39
+ return `mcpcfg_${userId}_${scope}_${configType}`;
36
40
  };
37
41
 
38
- /** Get all MCP configs for a user (localhost + remote). */
39
- getAllConfigs = async (userId: string): Promise<McpConfigRecord[]> => {
42
+ /** Get all MCP configs for a user at a given scope. */
43
+ getAllConfigs = async (userId: string, projectId?: string | null): Promise<McpConfigRecord[]> => {
40
44
  const db = this.getDb();
41
- return db.select().from(userMcpConfigs).where(eq(userMcpConfigs.userId, userId));
45
+ const scopeCondition = projectId
46
+ ? eq(userMcpConfigs.projectId, projectId)
47
+ : isNull(userMcpConfigs.projectId);
48
+ return db.select().from(userMcpConfigs).where(
49
+ and(eq(userMcpConfigs.userId, userId), scopeCondition),
50
+ );
42
51
  };
43
52
 
44
- /** Get a single config by user and type. */
45
- getConfig = async (userId: string, configType: string): Promise<McpConfigRecord | null> => {
53
+ /** Get a single config by user, scope, and type. */
54
+ getConfig = async (userId: string, configType: string, projectId?: string | null): Promise<McpConfigRecord | null> => {
46
55
  const db = this.getDb();
47
- const id = this.makeId(userId, configType);
56
+ const id = this.makeId(userId, configType, projectId);
48
57
  const [row] = await db.select().from(userMcpConfigs).where(eq(userMcpConfigs.id, id));
49
58
  return row || null;
50
59
  };
51
60
 
52
61
  /** Create or update a config. Validates JSON before storing. */
53
- upsertConfig = async (userId: string, configType: string, mcpServersJson: string): Promise<McpConfigRecord> => {
54
- // Validate JSON
62
+ upsertConfig = async (userId: string, configType: string, mcpServersJson: string, projectId?: string | null): Promise<McpConfigRecord> => {
55
63
  try {
56
64
  JSON.parse(mcpServersJson);
57
65
  } catch {
@@ -63,22 +71,23 @@ export class McpConfigService {
63
71
  }
64
72
 
65
73
  const db = this.getDb();
66
- const id = this.makeId(userId, configType);
74
+ const id = this.makeId(userId, configType, projectId);
67
75
  const now = new Date().toISOString();
68
76
 
69
- const existing = await this.getConfig(userId, configType);
77
+ const existing = await this.getConfig(userId, configType, projectId);
70
78
 
71
79
  if (existing) {
72
80
  await db.update(userMcpConfigs)
73
- .set({ mcpServersJson, updatedAt: now })
81
+ .set({mcpServersJson, updatedAt: now})
74
82
  .where(eq(userMcpConfigs.id, id));
75
- this.log('MCP_CONFIG', `Updated ${configType} config for user ${userId}`);
76
- return { ...existing, mcpServersJson, updatedAt: now };
83
+ this.log('MCP_CONFIG', `Updated ${configType} config for user ${userId} (project: ${projectId || 'global'})`);
84
+ return {...existing, mcpServersJson, updatedAt: now};
77
85
  }
78
86
 
79
87
  const record: McpConfigRecord = {
80
88
  id,
81
89
  userId,
90
+ projectId: projectId || null,
82
91
  configType,
83
92
  mcpServersJson,
84
93
  createdAt: now,
@@ -86,37 +95,51 @@ export class McpConfigService {
86
95
  };
87
96
 
88
97
  await db.insert(userMcpConfigs).values(record);
89
- this.log('MCP_CONFIG', `Created ${configType} config for user ${userId}`);
98
+ this.log('MCP_CONFIG', `Created ${configType} config for user ${userId} (project: ${projectId || 'global'})`);
90
99
  return record;
91
100
  };
92
101
 
93
102
  /** Delete a config. */
94
- deleteConfig = async (userId: string, configType: string): Promise<boolean> => {
103
+ deleteConfig = async (userId: string, configType: string, projectId?: string | null): Promise<boolean> => {
95
104
  const db = this.getDb();
96
- const id = this.makeId(userId, configType);
97
- const existing = await this.getConfig(userId, configType);
105
+ const id = this.makeId(userId, configType, projectId);
106
+ const existing = await this.getConfig(userId, configType, projectId);
98
107
  if (!existing) return false;
99
108
 
100
109
  await db.delete(userMcpConfigs).where(eq(userMcpConfigs.id, id));
101
- this.log('MCP_CONFIG', `Deleted ${configType} config for user ${userId}`);
110
+ this.log('MCP_CONFIG', `Deleted ${configType} config for user ${userId} (project: ${projectId || 'global'})`);
102
111
  return true;
103
112
  };
104
113
 
105
114
  /**
106
- * Build a merged mcpServers object for a user based on the current environment.
115
+ * Build a merged mcpServers object for a user + project based on the current environment.
116
+ * Merges global configs first, then project-specific configs on top (project overrides global).
107
117
  * In Docker: only remote configs are included.
108
118
  * Locally: both localhost and remote configs are merged.
109
119
  */
110
- buildMergedConfig = async (userId: string): Promise<Record<string, unknown> | null> => {
111
- const configs = await this.getAllConfigs(userId);
120
+ buildMergedConfig = async (userId: string, projectId?: string | null): Promise<Record<string, unknown> | null> => {
121
+ const db = this.getDb();
122
+
123
+ // Load global + project-scoped configs in one query
124
+ const condition = projectId
125
+ ? and(
126
+ eq(userMcpConfigs.userId, userId),
127
+ or(isNull(userMcpConfigs.projectId), eq(userMcpConfigs.projectId, projectId)),
128
+ )
129
+ : and(eq(userMcpConfigs.userId, userId), isNull(userMcpConfigs.projectId));
130
+
131
+ const configs: McpConfigRecord[] = await db.select().from(userMcpConfigs).where(condition);
112
132
  if (configs.length === 0) return null;
113
133
 
114
- const isDocker = process.env.DOCKER === '1'
115
- || (await import('node:fs')).existsSync('/.dockerenv');
134
+ const isDocker = process.env.DOCKER === '1' || existsSync('/.dockerenv');
116
135
 
117
136
  const merged: Record<string, unknown> = {};
118
137
 
119
- for (const cfg of configs) {
138
+ // Apply global configs first, then project-specific (so project overrides global)
139
+ const globalConfigs = configs.filter(c => !c.projectId);
140
+ const projectConfigs = configs.filter(c => !!c.projectId);
141
+
142
+ for (const cfg of [...globalConfigs, ...projectConfigs]) {
120
143
  if (isDocker && cfg.configType === 'localhost') continue;
121
144
 
122
145
  try {
@@ -943,16 +943,18 @@ export class ApiClient {
943
943
 
944
944
  // --- MCP Config ---
945
945
 
946
- /** Fetch all MCP configs for the authenticated user. */
947
- getMcpConfigs = async (): Promise<{ configs: Array<{ id: string; userId: string; configType: string; mcpServersJson: string; createdAt: string; updatedAt: string }> }> => {
948
- const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/mcp-config`);
946
+ /** Fetch MCP configs for the authenticated user. Pass projectId for project-scoped, omit for global. */
947
+ getMcpConfigs = async (projectId?: string | null): Promise<{ configs: Array<{ id: string; userId: string; projectId: string | null; configType: string; mcpServersJson: string; createdAt: string; updatedAt: string }> }> => {
948
+ const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : '';
949
+ const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/mcp-config${qs}`);
949
950
  if (!resp.ok) throw new Error(`Failed to fetch MCP configs: ${resp.status}`);
950
951
  return resp.json();
951
952
  };
952
953
 
953
- /** Save (create or update) an MCP config for the authenticated user. */
954
- saveMcpConfig = async (configType: 'localhost' | 'remote', mcpServersJson: string): Promise<{ config: any }> => {
955
- const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/mcp-config/${configType}`, {
954
+ /** Save (create or update) an MCP config. Pass projectId for project-scoped, omit for global. */
955
+ saveMcpConfig = async (configType: 'localhost' | 'remote', mcpServersJson: string, projectId?: string | null): Promise<{ config: any }> => {
956
+ const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : '';
957
+ const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/mcp-config/${configType}${qs}`, {
956
958
  method: 'PUT',
957
959
  headers: { 'Content-Type': 'application/json' },
958
960
  body: JSON.stringify({ mcpServersJson }),
@@ -964,9 +966,10 @@ export class ApiClient {
964
966
  return resp.json();
965
967
  };
966
968
 
967
- /** Delete an MCP config for the authenticated user. */
968
- deleteMcpConfig = async (configType: 'localhost' | 'remote'): Promise<void> => {
969
- const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/mcp-config/${configType}`, {
969
+ /** Delete an MCP config. Pass projectId for project-scoped, omit for global. */
970
+ deleteMcpConfig = async (configType: 'localhost' | 'remote', projectId?: string | null): Promise<void> => {
971
+ const qs = projectId ? `?projectId=${encodeURIComponent(projectId)}` : '';
972
+ const resp = await this.fetchWithRefresh(`${this.baseUrl}/api/mcp-config/${configType}${qs}`, {
970
973
  method: 'DELETE',
971
974
  });
972
975
  if (!resp.ok) {
@@ -1,18 +1,21 @@
1
1
  /**
2
2
  * McpConfigModal — Modal dialog for editing per-user MCP server configurations.
3
- * Two tabs: Localhost (for local dev) and Remote (for Docker/cloud deployments).
3
+ * Scope toggle: Global (user-level) or Project (project-scoped).
4
+ * Two tabs per scope: Localhost (local dev only) and Remote (all environments).
4
5
  * Each tab has a JSON textarea editor with validation and save functionality.
5
6
  */
6
7
 
7
- import { useCallback, useEffect, useState } from 'react';
8
- import { X } from 'lucide-react';
9
- import { apiClient } from '../api/client';
8
+ import {useCallback, useEffect, useState} from 'react';
9
+ import {Globe, X} from 'lucide-react';
10
+ import {apiClient} from '../api/client';
11
+ import {useProjectStore} from '../stores/useProjectStore';
10
12
 
11
13
  interface McpConfigModalProps {
12
14
  onClose: () => void;
13
15
  }
14
16
 
15
17
  type ConfigTab = 'localhost' | 'remote';
18
+ type Scope = 'global' | 'project';
16
19
 
17
20
  const PLACEHOLDER_CONFIG = JSON.stringify(
18
21
  {
@@ -24,10 +27,18 @@ const PLACEHOLDER_CONFIG = JSON.stringify(
24
27
  2,
25
28
  );
26
29
 
27
- export function McpConfigModal({ onClose }: McpConfigModalProps) {
30
+ export function McpConfigModal({onClose}: McpConfigModalProps) {
31
+ const selectedProjectId = useProjectStore((s) => s.selectedProjectId);
32
+
33
+ const [scope, setScope] = useState<Scope>('global');
28
34
  const [activeTab, setActiveTab] = useState<ConfigTab>('localhost');
29
- const [localhostJson, setLocalhostJson] = useState('{}');
30
- const [remoteJson, setRemoteJson] = useState('{}');
35
+
36
+ // Separate state for global and project configs
37
+ const [globalLocalhostJson, setGlobalLocalhostJson] = useState('{}');
38
+ const [globalRemoteJson, setGlobalRemoteJson] = useState('{}');
39
+ const [projectLocalhostJson, setProjectLocalhostJson] = useState('{}');
40
+ const [projectRemoteJson, setProjectRemoteJson] = useState('{}');
41
+
31
42
  const [saving, setSaving] = useState(false);
32
43
  const [error, setError] = useState<string | null>(null);
33
44
  const [success, setSuccess] = useState<string | null>(null);
@@ -36,11 +47,22 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
36
47
  useEffect(() => {
37
48
  const load = async () => {
38
49
  try {
39
- const { configs } = await apiClient.getMcpConfigs();
40
- for (const cfg of configs) {
50
+ // Load global configs
51
+ const {configs: globalConfigs} = await apiClient.getMcpConfigs();
52
+ for (const cfg of globalConfigs) {
41
53
  const formatted = JSON.stringify(JSON.parse(cfg.mcpServersJson), null, 2);
42
- if (cfg.configType === 'localhost') setLocalhostJson(formatted);
43
- if (cfg.configType === 'remote') setRemoteJson(formatted);
54
+ if (cfg.configType === 'localhost') setGlobalLocalhostJson(formatted);
55
+ if (cfg.configType === 'remote') setGlobalRemoteJson(formatted);
56
+ }
57
+
58
+ // Load project configs if a project is selected
59
+ if (selectedProjectId) {
60
+ const {configs: projectConfigs} = await apiClient.getMcpConfigs(selectedProjectId);
61
+ for (const cfg of projectConfigs) {
62
+ const formatted = JSON.stringify(JSON.parse(cfg.mcpServersJson), null, 2);
63
+ if (cfg.configType === 'localhost') setProjectLocalhostJson(formatted);
64
+ if (cfg.configType === 'remote') setProjectRemoteJson(formatted);
65
+ }
44
66
  }
45
67
  } catch (err: any) {
46
68
  setError(`Failed to load configs: ${err.message}`);
@@ -49,10 +71,24 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
49
71
  }
50
72
  };
51
73
  load();
52
- }, []);
74
+ }, [selectedProjectId]);
75
+
76
+ const getCurrentJson = (): string => {
77
+ if (scope === 'global') return activeTab === 'localhost' ? globalLocalhostJson : globalRemoteJson;
78
+ return activeTab === 'localhost' ? projectLocalhostJson : projectRemoteJson;
79
+ };
53
80
 
54
- const currentJson = activeTab === 'localhost' ? localhostJson : remoteJson;
55
- const setCurrentJson = activeTab === 'localhost' ? setLocalhostJson : setRemoteJson;
81
+ const setCurrentJson = (value: string) => {
82
+ if (scope === 'global') {
83
+ if (activeTab === 'localhost') setGlobalLocalhostJson(value);
84
+ else setGlobalRemoteJson(value);
85
+ } else {
86
+ if (activeTab === 'localhost') setProjectLocalhostJson(value);
87
+ else setProjectRemoteJson(value);
88
+ }
89
+ };
90
+
91
+ const currentJson = getCurrentJson();
56
92
 
57
93
  const validateJson = useCallback((json: string): string | null => {
58
94
  try {
@@ -67,7 +103,7 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
67
103
  }, []);
68
104
 
69
105
  const handleSave = useCallback(async () => {
70
- const jsonToSave = activeTab === 'localhost' ? localhostJson : remoteJson;
106
+ const jsonToSave = getCurrentJson();
71
107
  const validationError = validateJson(jsonToSave);
72
108
  if (validationError) {
73
109
  setError(validationError);
@@ -78,16 +114,20 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
78
114
  setError(null);
79
115
  setSuccess(null);
80
116
 
117
+ const projectId = scope === 'project' ? selectedProjectId : null;
118
+
81
119
  try {
82
- await apiClient.saveMcpConfig(activeTab, jsonToSave);
83
- setSuccess(`${activeTab === 'localhost' ? 'Localhost' : 'Remote'} config saved`);
120
+ await apiClient.saveMcpConfig(activeTab, jsonToSave, projectId);
121
+ const scopeLabel = scope === 'project' ? 'Project' : 'Global';
122
+ const typeLabel = activeTab === 'localhost' ? 'localhost' : 'remote';
123
+ setSuccess(`${scopeLabel} ${typeLabel} config saved`);
84
124
  setTimeout(() => setSuccess(null), 3000);
85
125
  } catch (err: any) {
86
126
  setError(err.message);
87
127
  } finally {
88
128
  setSaving(false);
89
129
  }
90
- }, [activeTab, localhostJson, remoteJson, validateJson]);
130
+ }, [scope, activeTab, selectedProjectId, globalLocalhostJson, globalRemoteJson, projectLocalhostJson, projectRemoteJson, validateJson]);
91
131
 
92
132
  const handleKeyDown = useCallback(
93
133
  (e: React.KeyboardEvent) => {
@@ -105,7 +145,9 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
105
145
  return (
106
146
  <div
107
147
  className="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
108
- onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
148
+ onClick={(e) => {
149
+ if (e.target === e.currentTarget) onClose();
150
+ }}
109
151
  onKeyDown={handleKeyDown}
110
152
  >
111
153
  <div className="bg-surface border border-edge rounded-lg shadow-xl w-[640px] max-h-[80vh] flex flex-col">
@@ -121,11 +163,63 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
121
163
  </button>
122
164
  </div>
123
165
 
166
+ {/* Scope toggle */}
167
+ <div className="flex items-center gap-1 px-4 py-2 border-b border-edge">
168
+ <span className="text-[11px] text-content-muted mr-2">Scope:</span>
169
+ <button
170
+ type="button"
171
+ className={`h-6 px-2.5 rounded text-[11px] font-mono transition-colors ${
172
+ scope === 'global'
173
+ ? 'bg-accent text-white'
174
+ : 'text-content-muted bg-transparent border border-edge hover:bg-surface-raised'
175
+ }`}
176
+ onClick={() => {
177
+ setScope('global');
178
+ setError(null);
179
+ setSuccess(null);
180
+ }}
181
+ >
182
+ Global
183
+ </button>
184
+ <button
185
+ type="button"
186
+ className={`h-6 px-2.5 rounded text-[11px] font-mono transition-colors ${
187
+ scope === 'project'
188
+ ? 'bg-accent text-white'
189
+ : 'text-content-muted bg-transparent border border-edge hover:bg-surface-raised'
190
+ }`}
191
+ onClick={() => {
192
+ setScope('project');
193
+ setError(null);
194
+ setSuccess(null);
195
+ }}
196
+ disabled={!selectedProjectId}
197
+ title={selectedProjectId ? `Project: ${selectedProjectId}` : 'No project selected'}
198
+ >
199
+ Project
200
+ </button>
201
+ {scope === 'project' && selectedProjectId && (
202
+ <span className="text-[10px] text-content-muted ml-1 font-mono truncate max-w-[200px]">
203
+ {selectedProjectId}
204
+ </span>
205
+ )}
206
+ </div>
207
+
124
208
  {/* Description */}
125
209
  <div className="px-4 py-2 text-[11px] text-content-muted border-b border-edge">
126
- Configure MCP servers for your chat sessions. <strong>Localhost</strong> servers are only
127
- used in local development. <strong>Remote</strong> servers are used in all environments
128
- (local + Docker).
210
+ {scope === 'global' ? (
211
+ <>
212
+ <strong>Global</strong> configs apply to all projects for your user.{' '}
213
+ Project-specific configs are merged on top (project overrides global).
214
+ </>
215
+ ) : (
216
+ <>
217
+ <strong>Project</strong> configs only apply to this project and override global configs
218
+ with the same server name.
219
+ </>
220
+ )}
221
+ {' '}<strong>Localhost</strong> servers are excluded in Docker.{' '}
222
+ <strong>Remote</strong> servers are used everywhere.
129
223
  </div>
130
224
 
131
225
  {/* Tabs */}
@@ -139,7 +233,11 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
139
233
  ? 'text-accent border-b-2 border-accent bg-surface-alt'
140
234
  : 'text-content-muted hover:text-content hover:bg-surface-raised'
141
235
  }`}
142
- onClick={() => { setActiveTab(tab); setError(null); setSuccess(null); }}
236
+ onClick={() => {
237
+ setActiveTab(tab);
238
+ setError(null);
239
+ setSuccess(null);
240
+ }}
143
241
  >
144
242
  {tab === 'localhost' ? 'Localhost' : 'Remote'}
145
243
  </button>
@@ -167,12 +265,13 @@ export function McpConfigModal({ onClose }: McpConfigModalProps) {
167
265
  placeholder={PLACEHOLDER_CONFIG}
168
266
  spellCheck={false}
169
267
  />
170
- {jsonError && (
171
- <p className="text-[11px] text-error mt-1">{jsonError}</p>
172
- )}
268
+ {jsonError && <p className="text-[11px] text-error mt-1">{jsonError}</p>}
173
269
  <p className="text-[10px] text-content-muted mt-1.5">
174
- Enter the content of the <code className="bg-surface-raised px-1 rounded">mcpServers</code> object.
175
- Example: <code className="bg-surface-raised px-1 rounded">{'{ "jetbrains": { "url": "http://localhost:6637/sse" } }'}</code>
270
+ Enter the content of the{' '}
271
+ <code className="bg-surface-raised px-1 rounded">mcpServers</code> object. Example:{' '}
272
+ <code className="bg-surface-raised px-1 rounded">
273
+ {'{ "jetbrains": { "url": "http://localhost:6637/sse" } }'}
274
+ </code>
176
275
  </p>
177
276
  </>
178
277
  )}
@@ -0,0 +1 @@
1
+ ALTER TABLE `user_mcp_configs` ADD `project_id` text;