MCP modal

This commit is contained in:
andrepimenta
2025-07-09 14:32:12 +01:00
parent 521f9a7d68
commit 83584fff60
3 changed files with 464 additions and 49 deletions

View File

@@ -286,6 +286,15 @@ class ClaudeChatProvider {
case 'addPermission':
this._addPermission(message.toolName, message.command);
return;
case 'loadMCPServers':
this._loadMCPServers();
return;
case 'saveMCPServer':
this._saveMCPServer(message.name, message.config);
return;
case 'deleteMCPServer':
this._deleteMCPServer(message.name);
return;
}
}
@@ -1451,6 +1460,118 @@ class ClaudeChatProvider {
}
}
private async _loadMCPServers(): Promise<void> {
try {
const mcpConfigPath = this.getMCPConfigPath();
if (!mcpConfigPath) {
this._sendAndSaveMessage({ type: 'mcpServers', data: {} });
return;
}
const mcpConfigUri = vscode.Uri.file(mcpConfigPath);
let mcpConfig: any = { mcpServers: {} };
try {
const content = await vscode.workspace.fs.readFile(mcpConfigUri);
mcpConfig = JSON.parse(new TextDecoder().decode(content));
} catch {
// File doesn't exist, return empty servers
}
this._postMessage({ type: 'mcpServers', data: mcpConfig.mcpServers || {} });
} catch (error) {
console.error('Error loading MCP servers:', error);
this._postMessage({ type: 'mcpServerError', data: { error: 'Failed to load MCP servers' } });
}
}
private async _saveMCPServer(name: string, config: any): Promise<void> {
try {
const mcpConfigPath = this.getMCPConfigPath();
if (!mcpConfigPath) {
this._postMessage({ type: 'mcpServerError', data: { error: 'Storage path not available' } });
return;
}
const mcpConfigUri = vscode.Uri.file(mcpConfigPath);
let mcpConfig: any = { mcpServers: {} };
// Load existing config
try {
const content = await vscode.workspace.fs.readFile(mcpConfigUri);
mcpConfig = JSON.parse(new TextDecoder().decode(content));
} catch {
// File doesn't exist, use default structure
}
// Ensure mcpServers exists
if (!mcpConfig.mcpServers) {
mcpConfig.mcpServers = {};
}
// Add/update the server
mcpConfig.mcpServers[name] = config;
// Ensure directory exists
const mcpDir = vscode.Uri.file(path.dirname(mcpConfigPath));
try {
await vscode.workspace.fs.stat(mcpDir);
} catch {
await vscode.workspace.fs.createDirectory(mcpDir);
}
// Save the config
const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2));
await vscode.workspace.fs.writeFile(mcpConfigUri, configContent);
this._postMessage({ type: 'mcpServerSaved', data: { name } });
console.log(`Saved MCP server: ${name}`);
} catch (error) {
console.error('Error saving MCP server:', error);
this._postMessage({ type: 'mcpServerError', data: { error: 'Failed to save MCP server' } });
}
}
private async _deleteMCPServer(name: string): Promise<void> {
try {
const mcpConfigPath = this.getMCPConfigPath();
if (!mcpConfigPath) {
this._postMessage({ type: 'mcpServerError', data: { error: 'Storage path not available' } });
return;
}
const mcpConfigUri = vscode.Uri.file(mcpConfigPath);
let mcpConfig: any = { mcpServers: {} };
// Load existing config
try {
const content = await vscode.workspace.fs.readFile(mcpConfigUri);
mcpConfig = JSON.parse(new TextDecoder().decode(content));
} catch {
// File doesn't exist, nothing to delete
this._postMessage({ type: 'mcpServerError', data: { error: 'MCP config file not found' } });
return;
}
// Delete the server
if (mcpConfig.mcpServers && mcpConfig.mcpServers[name]) {
delete mcpConfig.mcpServers[name];
// Save the updated config
const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2));
await vscode.workspace.fs.writeFile(mcpConfigUri, configContent);
this._postMessage({ type: 'mcpServerDeleted', data: { name } });
console.log(`Deleted MCP server: ${name}`);
} else {
this._postMessage({ type: 'mcpServerError', data: { error: `Server '${name}' not found` } });
}
} catch (error) {
console.error('Error deleting MCP server:', error);
this._postMessage({ type: 'mcpServerError', data: { error: 'Failed to delete MCP server' } });
}
}
public getMCPConfigPath(): string | undefined {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) {return undefined;}

View File

@@ -2307,6 +2307,129 @@ const styles = `
color: var(--vscode-foreground);
opacity: 0.8;
}
/* MCP Servers styles */
.mcp-servers-list {
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.mcp-server-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
margin-bottom: 8px;
background-color: var(--vscode-editor-background);
}
.server-info {
flex: 1;
}
.server-name {
font-weight: 600;
font-size: 14px;
color: var(--vscode-foreground);
margin-bottom: 4px;
}
.server-type {
display: inline-block;
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
margin-bottom: 4px;
}
.server-config {
font-size: 12px;
color: var(--vscode-descriptionForeground);
opacity: 0.8;
}
.server-delete-btn {
padding: 4px 8px;
font-size: 12px;
color: var(--vscode-errorForeground);
border-color: var(--vscode-errorForeground);
}
.server-delete-btn:hover {
background-color: var(--vscode-inputValidation-errorBackground);
border-color: var(--vscode-errorForeground);
}
.mcp-add-server {
text-align: center;
margin-bottom: 20px;
}
.mcp-add-form {
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 16px;
margin-top: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 13px;
color: var(--vscode-foreground);
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-size: 13px;
font-family: var(--vscode-font-family);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
}
.form-group textarea {
resize: vertical;
min-height: 60px;
}
.form-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 20px;
}
.no-servers {
text-align: center;
color: var(--vscode-descriptionForeground);
font-style: italic;
padding: 40px 20px;
}
</style>`
export default styles

269
src/ui.ts
View File

@@ -75,8 +75,8 @@ const html = `<!DOCTYPE html>
<path d="M1 2.5l3 3 3-3"></path>
</svg>
</button>
<button class="tools-btn" onclick="showToolsModal()" title="Configure tools">
Tools: All
<button class="tools-btn" onclick="showMCPModal()" title="Configure MCP servers">
MCP
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M1 2.5l3 3 3-3"></path>
</svg>
@@ -148,56 +148,55 @@ const html = `<!DOCTYPE html>
</div>
</div>
<!-- Tools modal -->
<div id="toolsModal" class="tools-modal" style="display: none;">
<!-- MCP Servers modal -->
<div id="mcpModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Claude Code Tools</span>
<button class="tools-close-btn" onclick="hideToolsModal()">✕</button>
<span>MCP Servers</span>
<button class="tools-close-btn" onclick="hideMCPModal()">✕</button>
</div>
<div class="tools-beta-warning">
In Beta: All tools are enabled by default. Use at your own risk.
<div class="mcp-servers-list" id="mcpServersList">
<!-- MCP servers will be loaded here -->
</div>
<div id="toolsList" class="tools-list">
<div class="tool-item">
<input type="checkbox" id="tool-bash" checked disabled>
<label for="tool-bash">Bash - Execute shell commands</label>
<div class="mcp-add-server">
<button class="btn outlined" onclick="showAddServerForm()" id="addServerBtn">+ Add MCP Server</button>
</div>
<div class="mcp-add-form" id="addServerForm" style="display: none;">
<div class="form-group">
<label for="serverName">Server Name:</label>
<input type="text" id="serverName" placeholder="my-server" required>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-read" checked disabled>
<label for="tool-read">Read - Read file contents</label>
<div class="form-group">
<label for="serverType">Server Type:</label>
<select id="serverType" onchange="updateServerForm()">
<option value="stdio">stdio</option>
<option value="http">HTTP</option>
<option value="sse">SSE</option>
</select>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-edit" checked disabled>
<label for="tool-edit">Edit - Modify files</label>
<div class="form-group" id="commandGroup">
<label for="serverCommand">Command:</label>
<input type="text" id="serverCommand" placeholder="/path/to/server">
</div>
<div class="tool-item">
<input type="checkbox" id="tool-write" checked disabled>
<label for="tool-write">Write - Create new files</label>
<div class="form-group" id="urlGroup" style="display: none;">
<label for="serverUrl">URL:</label>
<input type="text" id="serverUrl" placeholder="https://example.com/mcp">
</div>
<div class="tool-item">
<input type="checkbox" id="tool-glob" checked disabled>
<label for="tool-glob">Glob - Find files by pattern</label>
<div class="form-group" id="argsGroup">
<label for="serverArgs">Arguments (one per line):</label>
<textarea id="serverArgs" placeholder="--api-key&#10;abc123" rows="3"></textarea>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-grep" checked disabled>
<label for="tool-grep">Grep - Search file contents</label>
<div class="form-group" id="envGroup">
<label for="serverEnv">Environment Variables (KEY=value, one per line):</label>
<textarea id="serverEnv" placeholder="API_KEY=123&#10;CACHE_DIR=/tmp" rows="3"></textarea>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-ls" checked disabled>
<label for="tool-ls">LS - List directory contents</label>
<div class="form-group" id="headersGroup" style="display: none;">
<label for="serverHeaders">Headers (KEY=value, one per line):</label>
<textarea id="serverHeaders" placeholder="Authorization=Bearer token&#10;X-API-Key=key" rows="3"></textarea>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-multiedit" checked disabled>
<label for="tool-multiedit">MultiEdit - Edit multiple files</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-websearch" checked disabled>
<label for="tool-websearch">WebSearch - Search the web</label>
</div>
<div class="tool-item">
<input type="checkbox" id="tool-webfetch" checked disabled>
<label for="tool-webfetch">WebFetch - Fetch web content</label>
<div class="form-buttons">
<button class="btn" onclick="saveMCPServer()">Add Server</button>
<button class="btn outlined" onclick="hideAddServerForm()">Cancel</button>
</div>
</div>
</div>
@@ -1523,8 +1522,10 @@ const html = `<!DOCTYPE html>
});
// Tools modal functions
function showToolsModal() {
document.getElementById('toolsModal').style.display = 'flex';
function showMCPModal() {
document.getElementById('mcpModal').style.display = 'flex';
// Load existing MCP servers
loadMCPServers();
}
function updateYoloWarning() {
@@ -1575,17 +1576,173 @@ const html = `<!DOCTYPE html>
}
}
function hideToolsModal() {
document.getElementById('toolsModal').style.display = 'none';
function hideMCPModal() {
document.getElementById('mcpModal').style.display = 'none';
hideAddServerForm();
}
// Close tools modal when clicking outside
document.getElementById('toolsModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('toolsModal')) {
hideToolsModal();
// Close MCP modal when clicking outside
document.getElementById('mcpModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('mcpModal')) {
hideMCPModal();
}
});
// MCP Server management functions
function loadMCPServers() {
vscode.postMessage({ type: 'loadMCPServers' });
}
function showAddServerForm() {
document.getElementById('addServerBtn').style.display = 'none';
document.getElementById('addServerForm').style.display = 'block';
}
function hideAddServerForm() {
document.getElementById('addServerBtn').style.display = 'block';
document.getElementById('addServerForm').style.display = 'none';
// Clear form
document.getElementById('serverName').value = '';
document.getElementById('serverCommand').value = '';
document.getElementById('serverUrl').value = '';
document.getElementById('serverArgs').value = '';
document.getElementById('serverEnv').value = '';
document.getElementById('serverHeaders').value = '';
document.getElementById('serverType').value = 'stdio';
updateServerForm();
}
function updateServerForm() {
const serverType = document.getElementById('serverType').value;
const commandGroup = document.getElementById('commandGroup');
const urlGroup = document.getElementById('urlGroup');
const argsGroup = document.getElementById('argsGroup');
const envGroup = document.getElementById('envGroup');
const headersGroup = document.getElementById('headersGroup');
if (serverType === 'stdio') {
commandGroup.style.display = 'block';
urlGroup.style.display = 'none';
argsGroup.style.display = 'block';
envGroup.style.display = 'block';
headersGroup.style.display = 'none';
} else if (serverType === 'http' || serverType === 'sse') {
commandGroup.style.display = 'none';
urlGroup.style.display = 'block';
argsGroup.style.display = 'none';
envGroup.style.display = 'none';
headersGroup.style.display = 'block';
}
}
function saveMCPServer() {
const name = document.getElementById('serverName').value.trim();
const type = document.getElementById('serverType').value;
if (!name) {
alert('Server name is required');
return;
}
const serverConfig = { type };
if (type === 'stdio') {
const command = document.getElementById('serverCommand').value.trim();
if (!command) {
alert('Command is required for stdio servers');
return;
}
serverConfig.command = command;
const argsText = document.getElementById('serverArgs').value.trim();
if (argsText) {
serverConfig.args = argsText.split('\\n').filter(line => line.trim());
}
const envText = document.getElementById('serverEnv').value.trim();
if (envText) {
serverConfig.env = {};
envText.split('\\n').forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
serverConfig.env[key.trim()] = valueParts.join('=').trim();
}
});
}
} else if (type === 'http' || type === 'sse') {
const url = document.getElementById('serverUrl').value.trim();
if (!url) {
alert('URL is required for HTTP/SSE servers');
return;
}
serverConfig.url = url;
const headersText = document.getElementById('serverHeaders').value.trim();
if (headersText) {
serverConfig.headers = {};
headersText.split('\\n').forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
serverConfig.headers[key.trim()] = valueParts.join('=').trim();
}
});
}
}
vscode.postMessage({
type: 'saveMCPServer',
name: name,
config: serverConfig
});
hideAddServerForm();
}
function deleteMCPServer(serverName) {
if (confirm(\`Are you sure you want to delete the server "\${serverName}"?\`)) {
vscode.postMessage({
type: 'deleteMCPServer',
name: serverName
});
}
}
function displayMCPServers(servers) {
const serversList = document.getElementById('mcpServersList');
serversList.innerHTML = '';
if (Object.keys(servers).length === 0) {
serversList.innerHTML = '<div class="no-servers">No MCP servers configured</div>';
return;
}
for (const [name, config] of Object.entries(servers)) {
const serverItem = document.createElement('div');
serverItem.className = 'mcp-server-item';
let configDisplay = '';
if (config.type === 'stdio') {
configDisplay = \`Command: \${config.command}\`;
if (config.args) {
configDisplay += \`<br>Args: \${config.args.join(' ')}\`;
}
} else {
configDisplay = \`URL: \${config.url}\`;
}
serverItem.innerHTML = \`
<div class="server-info">
<div class="server-name">\${name}</div>
<div class="server-type">\${config.type.toUpperCase()}</div>
<div class="server-config">\${configDisplay}</div>
</div>
<button class="btn outlined server-delete-btn" onclick="deleteMCPServer('\${name}')">Delete</button>
\`;
serversList.appendChild(serverItem);
}
}
// Model selector functions
let currentModel = 'opus'; // Default model
@@ -2141,6 +2298,20 @@ const html = `<!DOCTYPE html>
case 'permissionRequest':
addPermissionRequestMessage(message.data);
break;
case 'mcpServers':
displayMCPServers(message.data);
break;
case 'mcpServerSaved':
loadMCPServers(); // Reload the servers list
addMessage('✅ MCP server "' + message.data.name + '" saved successfully', 'system');
break;
case 'mcpServerDeleted':
loadMCPServers(); // Reload the servers list
addMessage('✅ MCP server "' + message.data.name + '" deleted successfully', 'system');
break;
case 'mcpServerError':
addMessage('❌ Error with MCP server: ' + message.data.error, 'error');
break;
}
});