Skip to content

Commit 4763aaf

Browse files
committed
Add MCP server controls and settings
1 parent f6b43f1 commit 4763aaf

15 files changed

Lines changed: 1061 additions & 378 deletions

File tree

extensions/roopik/src/canvasPanel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,14 +1176,14 @@ export class CanvasPanel implements vscode.Disposable {
11761176
vscode.Uri.joinPath(this.extensionUri, 'webview', 'build', 'assets', 'componentView.css')
11771177
);
11781178

1179-
// CSP: Allow CDNs for imports in sandbox iframes (esm.sh, skypack, jsdelivr for Tailwind)
1179+
// CSP: Allow CDNs for imports in sandbox iframes (esm.sh, skypack, jsdelivr, tailwindcss)
11801180
const csp = `
11811181
default-src 'none';
11821182
style-src ${webview.cspSource} 'unsafe-inline';
1183-
script-src ${webview.cspSource} 'unsafe-inline' 'unsafe-eval' https://esm.sh https://cdn.skypack.dev https://cdn.jsdelivr.net;
1183+
script-src ${webview.cspSource} 'unsafe-inline' 'unsafe-eval' https://esm.sh https://cdn.skypack.dev https://cdn.jsdelivr.net https://cdn.tailwindcss.com;
11841184
font-src ${webview.cspSource} data:;
11851185
img-src ${webview.cspSource} data: https:;
1186-
connect-src https://esm.sh https://cdn.skypack.dev https://cdn.jsdelivr.net;
1186+
connect-src https://esm.sh https://cdn.skypack.dev https://cdn.jsdelivr.net https://cdn.tailwindcss.com;
11871187
frame-src blob: data: https:;
11881188
`;
11891189

extensions/roopik/webview/src/canvasView/components/SandboxCard/SandboxCard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ function generateSandboxHTML(bundledCode: string, componentId: string): string {
6363
<meta charset="UTF-8">
6464
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6565
<title>Roopik Component Sandbox</title>
66-
<!-- Tailwind CSS v4 Play CDN - enables Tailwind utility classes in all components -->
67-
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
66+
<!-- Tailwind CSS v3 Play CDN - enables Tailwind utility classes in all components -->
67+
<!-- Using v3 because it has JIT mode that watches for DOM changes (React renders after page load) -->
68+
<script src="https://cdn.tailwindcss.com"></script>
6869
<style>
6970
* { margin: 0; padding: 0; box-sizing: border-box; }
7071
body {

src/vs/workbench/contrib/preferences/browser/settingsLayout.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,38 @@ export const tocData: ITOCEntry<string> = {
324324
settings: ['security.workspace.*']
325325
}
326326
]
327+
},
328+
{
329+
id: 'roopik',
330+
label: localize('roopik', "Roopik"),
331+
settings: ['roopik.*'],
332+
children: [
333+
{
334+
id: 'roopik/general',
335+
label: localize('roopikGeneral', "General"),
336+
settings: ['roopik.general.*']
337+
},
338+
{
339+
id: 'roopik/mcp',
340+
label: localize('roopikMcp', "MCP Integrations"),
341+
settings: ['roopik.mcp.*']
342+
},
343+
{
344+
id: 'roopik/project',
345+
label: localize('roopikProject', "Project"),
346+
settings: ['roopik.project.*']
347+
},
348+
{
349+
id: 'roopik/browser',
350+
label: localize('roopikBrowser', "Browser"),
351+
settings: ['roopik.browser.*']
352+
},
353+
{
354+
id: 'roopik/canvas',
355+
label: localize('roopikCanvas', "Canvas"),
356+
settings: ['roopik.canvas.*']
357+
}
358+
]
327359
}
328360
]
329361
};

src/vs/workbench/contrib/roopik/browser/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { registerImportCommands } from './importCommands.js';
1616
import { registerBrowserCommands } from './browserCommands.js';
1717
import { registerComponentCommands } from './componentCommands.js';
1818
import { registerRoopikToolsCommands } from './roopikToolsCommands.js';
19+
import { registerMcpCommands } from './mcpCommands.js';
1920

2021
/**
2122
* Register all Roopik commands
@@ -29,6 +30,7 @@ export function registerAllCommands(): void {
2930
registerBrowserCommands();
3031
registerComponentCommands();
3132
registerRoopikToolsCommands(); // Bridge commands for agent roopik-dio
33+
registerMcpCommands(); // MCP server control commands
3234
}
3335

3436
// Re-export individual register functions for granular control
@@ -38,3 +40,4 @@ export { registerImportCommands } from './importCommands.js';
3840
export { registerBrowserCommands } from './browserCommands.js';
3941
export { registerComponentCommands } from './componentCommands.js';
4042
export { registerRoopikToolsCommands } from './roopikToolsCommands.js';
43+
export { registerMcpCommands } from './mcpCommands.js';
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Roopik. All rights reserved.
3+
* Licensed under the MIT License.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
/**
7+
* MCP Commands
8+
*
9+
* Commands for controlling MCP (Model Context Protocol) integrations.
10+
* - roopik.mcp.toggle: Toggle MCP server on/off
11+
* - roopik.mcp.showStatus: Show MCP integration status
12+
* - roopik.mcp.enableAgent: Enable a specific agent integration
13+
* - roopik.mcp.disableAgent: Disable a specific agent integration
14+
*/
15+
16+
import { localize, localize2 } from '../../../../../nls.js';
17+
import { registerAction2, Action2 } from '../../../../../platform/actions/common/actions.js';
18+
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
19+
import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js';
20+
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
21+
import { IMcpServerService, AgentId, AgentStatus } from '../../common/mcp/index.js';
22+
23+
/**
24+
* Agent display info for quick pick
25+
*/
26+
interface AgentQuickPickItem extends IQuickPickItem {
27+
agentId: AgentId;
28+
status: AgentStatus;
29+
}
30+
31+
/**
32+
* Register all MCP-related commands
33+
*/
34+
export function registerMcpCommands(): void {
35+
// Toggle MCP Server (master switch)
36+
registerAction2(class extends Action2 {
37+
constructor() {
38+
super({
39+
id: 'roopik.mcp.toggle',
40+
title: localize2('roopik.mcp.toggle', 'Toggle MCP Server'),
41+
category: localize2('roopik.category', 'Roopik'),
42+
f1: true
43+
});
44+
}
45+
46+
async run(accessor: ServicesAccessor): Promise<void> {
47+
const notificationService = accessor.get(INotificationService);
48+
const mcpServerService = accessor.get(IMcpServerService);
49+
50+
try {
51+
const isEnabled = await mcpServerService.isEnabled();
52+
await mcpServerService.setEnabled(!isEnabled);
53+
54+
if (!isEnabled) {
55+
notificationService.info('MCP Server enabled - AI agents can now connect');
56+
} else {
57+
notificationService.info('MCP Server disabled - All agent connections closed');
58+
}
59+
} catch (error) {
60+
notificationService.error(`Failed to toggle MCP Server: ${error}`);
61+
}
62+
}
63+
});
64+
65+
// Show MCP Status
66+
registerAction2(class extends Action2 {
67+
constructor() {
68+
super({
69+
id: 'roopik.mcp.showStatus',
70+
title: localize2('roopik.mcp.showStatus', 'Show MCP Status'),
71+
category: localize2('roopik.category', 'Roopik'),
72+
f1: true
73+
});
74+
}
75+
76+
async run(accessor: ServicesAccessor): Promise<void> {
77+
const notificationService = accessor.get(INotificationService);
78+
const mcpServerService = accessor.get(IMcpServerService);
79+
80+
try {
81+
const integrationStatus = await mcpServerService.getIntegrationStatus();
82+
const { server, agents } = integrationStatus;
83+
84+
// Build status message
85+
const serverStatus = server.running ? 'Running' : 'Stopped';
86+
const registeredAgents = agents.filter(a => a.registered);
87+
const installedAgents = agents.filter(a => a.installed);
88+
89+
let message = `MCP Server: ${serverStatus}`;
90+
if (server.running) {
91+
message += ` (HTTP: ${server.port}, WebSocket: ${server.wsPort})`;
92+
}
93+
message += `\n\nAgents: ${registeredAgents.length} registered, ${installedAgents.length} installed`;
94+
95+
if (registeredAgents.length > 0) {
96+
message += '\n\nRegistered:';
97+
for (const agent of registeredAgents) {
98+
message += `\n - ${agent.name}`;
99+
}
100+
}
101+
102+
notificationService.notify({
103+
severity: Severity.Info,
104+
message,
105+
sticky: true
106+
});
107+
} catch (error) {
108+
notificationService.error(`Failed to get MCP status: ${error}`);
109+
}
110+
}
111+
});
112+
113+
// Enable Agent Integration
114+
registerAction2(class extends Action2 {
115+
constructor() {
116+
super({
117+
id: 'roopik.mcp.enableAgent',
118+
title: localize2('roopik.mcp.enableAgent', 'Enable Agent Integration'),
119+
category: localize2('roopik.category', 'Roopik'),
120+
f1: true
121+
});
122+
}
123+
124+
async run(accessor: ServicesAccessor): Promise<void> {
125+
const quickInputService = accessor.get(IQuickInputService);
126+
const notificationService = accessor.get(INotificationService);
127+
const mcpServerService = accessor.get(IMcpServerService);
128+
129+
try {
130+
const agents = await mcpServerService.getAgentStatus();
131+
132+
// Filter to agents that are installed but not registered
133+
const availableAgents = agents.filter(a => a.installed && !a.registered);
134+
135+
if (availableAgents.length === 0) {
136+
notificationService.info('All installed agents are already enabled, or no agents are detected.');
137+
return;
138+
}
139+
140+
// Build quick pick items
141+
const items: AgentQuickPickItem[] = availableAgents.map(agent => ({
142+
label: agent.name,
143+
description: agent.installed ? 'Installed' : 'Not detected',
144+
agentId: agent.id,
145+
status: agent
146+
}));
147+
148+
const selected = await quickInputService.pick(items, {
149+
placeHolder: localize('selectAgentToEnable', 'Select an agent to enable')
150+
});
151+
152+
if (selected) {
153+
await mcpServerService.enableAgent(selected.agentId);
154+
notificationService.info(`Enabled ${selected.label}`);
155+
}
156+
} catch (error) {
157+
notificationService.error(`Failed to enable agent: ${error}`);
158+
}
159+
}
160+
});
161+
162+
// Disable Agent Integration
163+
registerAction2(class extends Action2 {
164+
constructor() {
165+
super({
166+
id: 'roopik.mcp.disableAgent',
167+
title: localize2('roopik.mcp.disableAgent', 'Disable Agent Integration'),
168+
category: localize2('roopik.category', 'Roopik'),
169+
f1: true
170+
});
171+
}
172+
173+
async run(accessor: ServicesAccessor): Promise<void> {
174+
const quickInputService = accessor.get(IQuickInputService);
175+
const notificationService = accessor.get(INotificationService);
176+
const mcpServerService = accessor.get(IMcpServerService);
177+
178+
try {
179+
const agents = await mcpServerService.getAgentStatus();
180+
181+
// Filter to agents that are registered
182+
const registeredAgents = agents.filter(a => a.registered);
183+
184+
if (registeredAgents.length === 0) {
185+
notificationService.info('No agents are currently enabled.');
186+
return;
187+
}
188+
189+
// Build quick pick items
190+
const items: AgentQuickPickItem[] = registeredAgents.map(agent => ({
191+
label: agent.name,
192+
description: 'Registered',
193+
agentId: agent.id,
194+
status: agent
195+
}));
196+
197+
const selected = await quickInputService.pick(items, {
198+
placeHolder: localize('selectAgentToDisable', 'Select an agent to disable')
199+
});
200+
201+
if (selected) {
202+
await mcpServerService.disableAgent(selected.agentId);
203+
notificationService.info(`Disabled ${selected.label}`);
204+
}
205+
} catch (error) {
206+
notificationService.error(`Failed to disable agent: ${error}`);
207+
}
208+
}
209+
});
210+
211+
// Sync Agent Registrations (re-sync with current settings)
212+
registerAction2(class extends Action2 {
213+
constructor() {
214+
super({
215+
id: 'roopik.mcp.syncAgents',
216+
title: localize2('roopik.mcp.syncAgents', 'Sync Agent Registrations'),
217+
category: localize2('roopik.category', 'Roopik'),
218+
f1: true
219+
});
220+
}
221+
222+
async run(accessor: ServicesAccessor): Promise<void> {
223+
const notificationService = accessor.get(INotificationService);
224+
const mcpServerService = accessor.get(IMcpServerService);
225+
226+
try {
227+
notificationService.info('Syncing agent registrations...');
228+
await mcpServerService.syncAgentRegistrations();
229+
230+
const agents = await mcpServerService.getAgentStatus();
231+
const registered = agents.filter(a => a.registered);
232+
233+
notificationService.info(`Agent sync complete: ${registered.length} agents registered`);
234+
} catch (error) {
235+
notificationService.error(`Failed to sync agents: ${error}`);
236+
}
237+
}
238+
});
239+
}

src/vs/workbench/contrib/roopik/browser/contributions/startupContribution.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
import { Disposable } from '../../../../../base/common/lifecycle.js';
1616
import { IWorkbenchContribution } from '../../../../common/contributions.js';
1717
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
18-
import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
1918
import { ILifecycleService, LifecyclePhase, StartupKind } from '../../../../services/lifecycle/common/lifecycle.js';
2019
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
2120
import { IOutputService } from '../../../../services/output/common/output.js';
2221
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
2322
import { IEditorService } from '../../../../services/editor/common/editorService.js';
24-
import { RoopikWelcomeEditor } from '../welcomeEditor.js';
23+
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
2524
import { RoopikWelcomeInput } from '../welcomeInput.js';
2625
import { ICanvasService } from '../../common/canvas/index.js';
2726
import { IComponentService } from '../../common/component/componentService.js';
@@ -37,11 +36,11 @@ export class RoopikStartupContribution extends Disposable implements IWorkbenchC
3736
constructor(
3837
@IEditorService private readonly editorService: IEditorService,
3938
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
40-
@IStorageService private readonly storageService: IStorageService,
4139
@ILifecycleService private readonly lifecycleService: ILifecycleService,
4240
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
4341
@IOutputService private readonly outputService: IOutputService,
4442
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
43+
@IConfigurationService private readonly configurationService: IConfigurationService,
4544
@ICanvasService private readonly canvasService: ICanvasService,
4645
@IComponentService private readonly componentService: IComponentService,
4746
@IProjectStorageService private readonly projectStorageService: IProjectStorageService,
@@ -151,7 +150,8 @@ export class RoopikStartupContribution extends Disposable implements IWorkbenchC
151150
private async openWelcomeOnStartup(): Promise<void> {
152151
await this.lifecycleService.when(LifecyclePhase.Restored);
153152

154-
const showOnStartup = this.storageService.getBoolean(RoopikWelcomeEditor.STORAGE_KEY, StorageScope.PROFILE, true);
153+
// Use configuration setting (roopik.general.showWelcomeOnStartup)
154+
const showOnStartup = this.configurationService.getValue<boolean>('roopik.general.showWelcomeOnStartup') ?? true;
155155

156156
if (showOnStartup && this.lifecycleService.startupKind !== StartupKind.ReloadedWindow) {
157157
if (!this.editorService.activeEditor || this.layoutService.openedDefaultEditors) {

0 commit comments

Comments
 (0)