@assistkick/create 1.19.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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/templates/assistkick-product-system/packages/backend/src/routes/mcp_config.ts +87 -0
  3. package/templates/assistkick-product-system/packages/backend/src/server.ts +8 -1
  4. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +7 -0
  5. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +26 -1
  6. package/templates/assistkick-product-system/packages/backend/src/services/mcp_config_service.ts +157 -0
  7. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +37 -0
  8. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +14 -1
  9. package/templates/assistkick-product-system/packages/frontend/src/components/McpConfigModal.tsx +307 -0
  10. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_superb_roxanne_simpson.sql +8 -0
  11. package/templates/assistkick-product-system/packages/shared/db/migrations/0002_noisy_maelstrom.sql +1 -0
  12. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +1019 -23
  13. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +997 -22
  14. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
  15. package/templates/assistkick-product-system/packages/shared/db/schema.ts +11 -0
  16. package/templates/assistkick-product-system/packages/shared/lib/app_use_flow.ts +484 -0
  17. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  18. package/templates/assistkick-product-system/packages/shared/tools/agent_builder.ts +341 -0
  19. package/templates/assistkick-product-system/packages/shared/tools/app_use_record.ts +268 -0
  20. package/templates/assistkick-product-system/packages/shared/tools/app_use_run.ts +348 -0
  21. package/templates/assistkick-product-system/packages/shared/tools/app_use_validate.ts +67 -0
  22. package/templates/assistkick-product-system/packages/shared/tools/workflow_builder.ts +754 -0
  23. package/templates/skills/assistkick-agent-builder/SKILL.md +168 -0
  24. package/templates/skills/assistkick-app-use/SKILL.md +296 -0
  25. package/templates/skills/assistkick-app-use/references/agent-browser.md +1156 -0
  26. package/templates/skills/assistkick-workflow-builder/SKILL.md +234 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistkick/create",
3
- "version": "1.19.0",
3
+ "version": "1.22.0",
4
4
  "description": "Scaffold assistkick-product-system into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * MCP config routes — per-user MCP server configuration management.
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
9
+ */
10
+
11
+ import { Router } from 'express';
12
+ import type { McpConfigService } from '../services/mcp_config_service.js';
13
+
14
+ interface McpConfigRoutesDeps {
15
+ mcpConfigService: McpConfigService;
16
+ log: (tag: string, ...args: any[]) => void;
17
+ }
18
+
19
+ export const createMcpConfigRoutes = ({ mcpConfigService, log }: McpConfigRoutesDeps): Router => {
20
+ const router: Router = Router();
21
+
22
+ // GET /api/mcp-config?projectId=...
23
+ router.get('/', async (req, res) => {
24
+ const userId = (req as any).user?.id;
25
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
26
+
27
+ const projectId = (req.query.projectId as string) || null;
28
+
29
+ try {
30
+ const configs = await mcpConfigService.getAllConfigs(userId, projectId);
31
+ res.json({ configs });
32
+ } catch (err: any) {
33
+ log('MCP_CONFIG', `List configs failed: ${err.message}`);
34
+ res.status(500).json({ error: 'Failed to list MCP configs' });
35
+ }
36
+ });
37
+
38
+ // PUT /api/mcp-config/:type?projectId=...
39
+ router.put('/:type', async (req, res) => {
40
+ const userId = (req as any).user?.id;
41
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
42
+
43
+ const configType = req.params.type;
44
+ const projectId = (req.query.projectId as string) || (req.body.projectId as string) || null;
45
+ const { mcpServersJson } = req.body;
46
+
47
+ if (!mcpServersJson || typeof mcpServersJson !== 'string') {
48
+ res.status(400).json({ error: 'mcpServersJson is required and must be a string' });
49
+ return;
50
+ }
51
+
52
+ try {
53
+ const config = await mcpConfigService.upsertConfig(userId, configType, mcpServersJson, projectId);
54
+ res.json({ config });
55
+ } catch (err: any) {
56
+ log('MCP_CONFIG', `Upsert config failed: ${err.message}`);
57
+ if (err.message.includes('Invalid JSON') || err.message.includes('configType must be')) {
58
+ res.status(400).json({ error: err.message });
59
+ return;
60
+ }
61
+ res.status(500).json({ error: 'Failed to save MCP config' });
62
+ }
63
+ });
64
+
65
+ // DELETE /api/mcp-config/:type?projectId=...
66
+ router.delete('/:type', async (req, res) => {
67
+ const userId = (req as any).user?.id;
68
+ if (!userId) { res.status(401).json({ error: 'Unauthorized' }); return; }
69
+
70
+ const configType = req.params.type;
71
+ const projectId = (req.query.projectId as string) || null;
72
+
73
+ try {
74
+ const deleted = await mcpConfigService.deleteConfig(userId, configType, projectId);
75
+ if (!deleted) {
76
+ res.status(404).json({ error: 'Config not found' });
77
+ return;
78
+ }
79
+ res.json({ success: true });
80
+ } catch (err: any) {
81
+ log('MCP_CONFIG', `Delete config failed: ${err.message}`);
82
+ res.status(500).json({ error: 'Failed to delete MCP config' });
83
+ }
84
+ });
85
+
86
+ return router;
87
+ };
@@ -54,6 +54,8 @@ import { createChatSessionRoutes } from './routes/chat_sessions.js';
54
54
  import { createChatUploadRoutes } from './routes/chat_upload.js';
55
55
  import { createChatFilesRoutes } from './routes/chat_files.js';
56
56
  import { createSkillRoutes } from './routes/skills.js';
57
+ import { McpConfigService } from './services/mcp_config_service.js';
58
+ import { createMcpConfigRoutes } from './routes/mcp_config.js';
57
59
  import { DevCommandDetector } from './services/dev_command_detector.js';
58
60
  import { PreviewServerManager } from './services/preview_server_manager.js';
59
61
  import { createPreviewApiRoutes, createPreviewProxyMiddleware } from './routes/preview.js';
@@ -186,6 +188,11 @@ waitForDb().then(async () => {
186
188
  logError('STARTUP', 'Startup tasks failed', err);
187
189
  });
188
190
 
191
+ // MCP config routes (per-user MCP server configuration)
192
+ const mcpConfigService = new McpConfigService({ getDb, log });
193
+ const mcpConfigRoutes = createMcpConfigRoutes({ mcpConfigService, log });
194
+ app.use('/api/mcp-config', authMiddleware.requireAuth, mcpConfigRoutes);
195
+
189
196
  // Chat v2 session management routes (authenticated)
190
197
  const chatSessionService = new ChatSessionService({ getDb, log });
191
198
  const chatMessageRepository = new ChatMessageRepository({ getDb, log });
@@ -262,7 +269,7 @@ const chatWss = new WebSocketServer({ noServer: true });
262
269
  const chatCliBridge = new ChatCliBridge({ projectRoot: paths.projectRoot, workspacesDir: paths.workspacesDir, log });
263
270
  const permissionService = new PermissionService({ getDb, log });
264
271
  const titleGeneratorService = new TitleGeneratorService({ log });
265
- const chatHandler = new ChatWsHandler({ wss: chatWss, authService, chatCliBridge, permissionService, chatMessageRepository, chatSessionService, titleGeneratorService, log });
272
+ const chatHandler = new ChatWsHandler({ wss: chatWss, authService, chatCliBridge, permissionService, chatMessageRepository, chatSessionService, titleGeneratorService, mcpConfigService, log });
266
273
 
267
274
  // Chat permission routes — used by MCP permission server (no auth — internal only)
268
275
  const chatPermissionRoutes = createChatPermissionRoutes({ permissionService, log });
@@ -33,6 +33,8 @@ export interface SpawnOptions {
33
33
  allowedTools?: string[];
34
34
  /** Compacted conversation context to prepend to the system prompt */
35
35
  continuationContext?: string;
36
+ /** Inline MCP config JSON string to pass via --mcp-config */
37
+ mcpConfigJson?: string;
36
38
  onEvent: (event: Record<string, unknown>) => void;
37
39
  }
38
40
 
@@ -90,6 +92,11 @@ export class ChatCliBridge {
90
92
  }
91
93
  }
92
94
 
95
+ // MCP server configuration (inline JSON string)
96
+ if (options.mcpConfigJson) {
97
+ args.push('--mcp-config', options.mcpConfigJson);
98
+ }
99
+
93
100
  // Prompt via stdin
94
101
  args.push('-p', '-');
95
102
 
@@ -29,6 +29,7 @@ import type { PermissionService, PermissionDecision, PermissionRequest } from '.
29
29
  import type { ChatMessageRepository } from './chat_message_repository.js';
30
30
  import type { ChatSessionService } from './chat_session_service.js';
31
31
  import type { TitleGeneratorService } from './title_generator_service.js';
32
+ import type { McpConfigService } from './mcp_config_service.js';
32
33
 
33
34
  export interface ChatWsHandlerDeps {
34
35
  wss: WebSocketServer;
@@ -38,6 +39,7 @@ export interface ChatWsHandlerDeps {
38
39
  chatMessageRepository?: ChatMessageRepository;
39
40
  chatSessionService?: ChatSessionService;
40
41
  titleGeneratorService?: TitleGeneratorService;
42
+ mcpConfigService?: McpConfigService;
41
43
  log: (tag: string, ...args: unknown[]) => void;
42
44
  }
43
45
 
@@ -237,6 +239,7 @@ export class ChatWsHandler {
237
239
  private readonly chatMessageRepository: ChatMessageRepository | null;
238
240
  private readonly chatSessionService: ChatSessionService | null;
239
241
  private readonly titleGeneratorService: TitleGeneratorService | null;
242
+ private readonly mcpConfigService: McpConfigService | null;
240
243
  private readonly log: ChatWsHandlerDeps['log'];
241
244
  private readonly activeStreams = new Map<WebSocket, Set<string>>();
242
245
 
@@ -249,7 +252,10 @@ export class ChatWsHandler {
249
252
  /** Track stream context per Claude session ID for message persistence. */
250
253
  private readonly streamContexts = new Map<string, StreamContext>();
251
254
 
252
- constructor({ wss, authService, chatCliBridge, permissionService, chatMessageRepository, chatSessionService, titleGeneratorService, log }: ChatWsHandlerDeps) {
255
+ /** Map WebSocket connections to their authenticated user ID. */
256
+ private readonly wsUserIds = new Map<WebSocket, string>();
257
+
258
+ constructor({ wss, authService, chatCliBridge, permissionService, chatMessageRepository, chatSessionService, titleGeneratorService, mcpConfigService, log }: ChatWsHandlerDeps) {
253
259
  this.wss = wss;
254
260
  this.authService = authService;
255
261
  this.chatCliBridge = chatCliBridge;
@@ -257,6 +263,7 @@ export class ChatWsHandler {
257
263
  this.chatMessageRepository = chatMessageRepository || null;
258
264
  this.chatSessionService = chatSessionService || null;
259
265
  this.titleGeneratorService = titleGeneratorService || null;
266
+ this.mcpConfigService = mcpConfigService || null;
260
267
  this.log = log;
261
268
 
262
269
  // Register the permission request handler to route requests to the right WebSocket
@@ -317,6 +324,7 @@ export class ChatWsHandler {
317
324
  }
318
325
 
319
326
  this.log('CHAT_WS', `Admin ${payload.email} connected`);
327
+ this.wsUserIds.set(ws, payload.sub);
320
328
 
321
329
  ws.on('message', (raw: Buffer | string) => {
322
330
  let msg: ChatClientMessage;
@@ -340,6 +348,7 @@ export class ChatWsHandler {
340
348
 
341
349
  ws.on('close', () => {
342
350
  this.log('CHAT_WS', `Admin ${payload.email} disconnected`);
351
+ this.wsUserIds.delete(ws);
343
352
  this.cancelActiveStream(ws);
344
353
  });
345
354
 
@@ -416,6 +425,21 @@ export class ChatWsHandler {
416
425
  }
417
426
  }
418
427
 
428
+ // Build inline MCP config JSON for this spawn (if user has MCP servers configured)
429
+ let mcpConfigJson: string | undefined;
430
+ const userId = this.wsUserIds.get(ws);
431
+ if (userId && this.mcpConfigService) {
432
+ try {
433
+ const mergedServers = await this.mcpConfigService.buildMergedConfig(userId, projectId);
434
+ if (mergedServers) {
435
+ mcpConfigJson = JSON.stringify({ mcpServers: mergedServers });
436
+ this.log('CHAT_WS', `MCP config: ${Object.keys(mergedServers).length} server(s) for user ${userId}`);
437
+ }
438
+ } catch (err: any) {
439
+ this.log('CHAT_WS', `Failed to build MCP config: ${err.message}`);
440
+ }
441
+ }
442
+
419
443
  try {
420
444
  const { completion, systemPrompt } = this.chatCliBridge.spawn({
421
445
  projectId,
@@ -425,6 +449,7 @@ export class ChatWsHandler {
425
449
  permissionMode,
426
450
  allowedTools: msg.allowedTools,
427
451
  continuationContext,
452
+ mcpConfigJson,
428
453
  onEvent: (event) => {
429
454
  // Forward to current WS (may have been re-attached after disconnect)
430
455
  const currentWs = this.sessionToWs.get(claudeSessionId);
@@ -0,0 +1,157 @@
1
+ /**
2
+ * McpConfigService — CRUD operations for per-user MCP server configurations.
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).
5
+ * Uses deterministic IDs to enforce uniqueness at the PK level.
6
+ */
7
+
8
+ import {eq, and, isNull, or} from 'drizzle-orm';
9
+ import {userMcpConfigs} from '@assistkick/shared/db/schema.js';
10
+ import {existsSync} from 'node:fs';
11
+
12
+ interface McpConfigServiceDeps {
13
+ getDb: () => any;
14
+ log: (tag: string, ...args: unknown[]) => void;
15
+ }
16
+
17
+ export interface McpConfigRecord {
18
+ id: string;
19
+ userId: string;
20
+ projectId: string | null;
21
+ configType: string;
22
+ mcpServersJson: string;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ }
26
+
27
+ export class McpConfigService {
28
+ private readonly getDb: () => any;
29
+ private readonly log: McpConfigServiceDeps['log'];
30
+
31
+ constructor({getDb, log}: McpConfigServiceDeps) {
32
+ this.getDb = getDb;
33
+ this.log = log;
34
+ }
35
+
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}`;
40
+ };
41
+
42
+ /** Get all MCP configs for a user at a given scope. */
43
+ getAllConfigs = async (userId: string, projectId?: string | null): Promise<McpConfigRecord[]> => {
44
+ const db = this.getDb();
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
+ );
51
+ };
52
+
53
+ /** Get a single config by user, scope, and type. */
54
+ getConfig = async (userId: string, configType: string, projectId?: string | null): Promise<McpConfigRecord | null> => {
55
+ const db = this.getDb();
56
+ const id = this.makeId(userId, configType, projectId);
57
+ const [row] = await db.select().from(userMcpConfigs).where(eq(userMcpConfigs.id, id));
58
+ return row || null;
59
+ };
60
+
61
+ /** Create or update a config. Validates JSON before storing. */
62
+ upsertConfig = async (userId: string, configType: string, mcpServersJson: string, projectId?: string | null): Promise<McpConfigRecord> => {
63
+ try {
64
+ JSON.parse(mcpServersJson);
65
+ } catch {
66
+ throw new Error('Invalid JSON in mcpServersJson');
67
+ }
68
+
69
+ if (configType !== 'localhost' && configType !== 'remote') {
70
+ throw new Error('configType must be "localhost" or "remote"');
71
+ }
72
+
73
+ const db = this.getDb();
74
+ const id = this.makeId(userId, configType, projectId);
75
+ const now = new Date().toISOString();
76
+
77
+ const existing = await this.getConfig(userId, configType, projectId);
78
+
79
+ if (existing) {
80
+ await db.update(userMcpConfigs)
81
+ .set({mcpServersJson, updatedAt: now})
82
+ .where(eq(userMcpConfigs.id, id));
83
+ this.log('MCP_CONFIG', `Updated ${configType} config for user ${userId} (project: ${projectId || 'global'})`);
84
+ return {...existing, mcpServersJson, updatedAt: now};
85
+ }
86
+
87
+ const record: McpConfigRecord = {
88
+ id,
89
+ userId,
90
+ projectId: projectId || null,
91
+ configType,
92
+ mcpServersJson,
93
+ createdAt: now,
94
+ updatedAt: now,
95
+ };
96
+
97
+ await db.insert(userMcpConfigs).values(record);
98
+ this.log('MCP_CONFIG', `Created ${configType} config for user ${userId} (project: ${projectId || 'global'})`);
99
+ return record;
100
+ };
101
+
102
+ /** Delete a config. */
103
+ deleteConfig = async (userId: string, configType: string, projectId?: string | null): Promise<boolean> => {
104
+ const db = this.getDb();
105
+ const id = this.makeId(userId, configType, projectId);
106
+ const existing = await this.getConfig(userId, configType, projectId);
107
+ if (!existing) return false;
108
+
109
+ await db.delete(userMcpConfigs).where(eq(userMcpConfigs.id, id));
110
+ this.log('MCP_CONFIG', `Deleted ${configType} config for user ${userId} (project: ${projectId || 'global'})`);
111
+ return true;
112
+ };
113
+
114
+ /**
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).
117
+ * In Docker: only remote configs are included.
118
+ * Locally: both localhost and remote configs are merged.
119
+ */
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);
132
+ if (configs.length === 0) return null;
133
+
134
+ const isDocker = process.env.DOCKER === '1' || existsSync('/.dockerenv');
135
+
136
+ const merged: Record<string, unknown> = {};
137
+
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]) {
143
+ if (isDocker && cfg.configType === 'localhost') continue;
144
+
145
+ try {
146
+ const servers = JSON.parse(cfg.mcpServersJson);
147
+ if (servers && typeof servers === 'object') {
148
+ Object.assign(merged, servers);
149
+ }
150
+ } catch {
151
+ // Skip invalid JSON entries
152
+ }
153
+ }
154
+
155
+ return Object.keys(merged).length > 0 ? merged : null;
156
+ };
157
+ }
@@ -941,6 +941,43 @@ export class ApiClient {
941
941
  return resp.json();
942
942
  };
943
943
 
944
+ // --- MCP Config ---
945
+
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}`);
950
+ if (!resp.ok) throw new Error(`Failed to fetch MCP configs: ${resp.status}`);
951
+ return resp.json();
952
+ };
953
+
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}`, {
958
+ method: 'PUT',
959
+ headers: { 'Content-Type': 'application/json' },
960
+ body: JSON.stringify({ mcpServersJson }),
961
+ });
962
+ if (!resp.ok) {
963
+ const err = await resp.json().catch(() => ({}));
964
+ throw new Error(err.error || `Failed to save MCP config: ${resp.status}`);
965
+ }
966
+ return resp.json();
967
+ };
968
+
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}`, {
973
+ method: 'DELETE',
974
+ });
975
+ if (!resp.ok) {
976
+ const err = await resp.json().catch(() => ({}));
977
+ throw new Error(err.error || `Failed to delete MCP config: ${resp.status}`);
978
+ }
979
+ };
980
+
944
981
  // --- File system ---
945
982
 
946
983
  fetchFileTree = async (projectId: string) => {
@@ -30,7 +30,8 @@ import {PermissionModeSelector} from './PermissionModeSelector';
30
30
  import {AttachmentManager} from '../lib/attachment_manager';
31
31
  import type {SkillInfo} from '../api/client';
32
32
  import {apiClient} from '../api/client';
33
- import {Check, ChevronDown, ChevronUp, Copy, Search, Settings2, X, Zap} from 'lucide-react';
33
+ import {Check, ChevronDown, ChevronUp, Copy, Search, Server, Settings2, X, Zap} from 'lucide-react';
34
+ import {McpConfigModal} from './McpConfigModal';
34
35
  import {IconButton} from './ds/IconButton';
35
36
  import {formatTokenCount} from '../lib/context_usage_helpers';
36
37
  import {useFileTreeCache} from '../hooks/use_file_tree_cache';
@@ -123,6 +124,7 @@ export function ChatView({ visible }: ChatViewProps) {
123
124
  useEffect(() => { localStorage.setItem('chat_allowed_tools', JSON.stringify(allowedTools)); }, [allowedTools]);
124
125
 
125
126
  const [showSettings, setShowSettings] = useState(false);
127
+ const [showMcpConfig, setShowMcpConfig] = useState(false);
126
128
  const [isNewSession, setIsNewSession] = useState(true);
127
129
  const [compacting, setCompacting] = useState(false);
128
130
  const [copiedSessionId, setCopiedSessionId] = useState(false);
@@ -757,6 +759,14 @@ export function ChatView({ visible }: ChatViewProps) {
757
759
  Compact
758
760
  </button>
759
761
  )}
762
+ <IconButton
763
+ label="MCP server config"
764
+ variant="ghost"
765
+ size="sm"
766
+ onClick={() => setShowMcpConfig(true)}
767
+ >
768
+ <Server size={14} strokeWidth={2} />
769
+ </IconButton>
760
770
  <IconButton
761
771
  label="Chat settings"
762
772
  variant={showSettings ? 'accent' : 'ghost'}
@@ -826,6 +836,9 @@ export function ChatView({ visible }: ChatViewProps) {
826
836
  }
827
837
  onRespond={handlePermissionResponse}
828
838
  />
839
+
840
+ {/* MCP Config modal */}
841
+ {showMcpConfig && <McpConfigModal onClose={() => setShowMcpConfig(false)} />}
829
842
  </div>
830
843
  );
831
844
  }