initial implementation

This commit is contained in:
andrepimenta
2025-07-08 21:04:10 +01:00
parent f501d2ddc4
commit 06eb335f7b
6 changed files with 984 additions and 4 deletions

View File

@@ -94,6 +94,8 @@ class ClaudeChatProvider {
private _backupRepoPath: string | undefined;
private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = [];
private _conversationsPath: string | undefined;
private _permissionRequestsPath: string | undefined;
private _permissionWatcher: vscode.FileSystemWatcher | undefined;
private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = [];
private _conversationStartTime: string | undefined;
private _conversationIndex: Array<{
@@ -117,6 +119,8 @@ class ClaudeChatProvider {
// Initialize backup repository and conversations
this._initializeBackupRepo();
this._initializeConversations();
this._initializeMCPConfig();
this._initializePermissions();
// Load conversation index from workspace state
this._conversationIndex = this._context.workspaceState.get('claude.conversationIndex', []);
@@ -393,10 +397,19 @@ class ClaudeChatProvider {
// Build command arguments with session management
const args = [
'-p',
'--output-format', 'stream-json', '--verbose',
'--dangerously-skip-permissions'
'--output-format', 'stream-json', '--verbose'
];
// Add MCP configuration for permissions
const mcpConfigPath = this.getMCPConfigPath();
if (mcpConfigPath) {
args.push('--mcp-config', mcpConfigPath);
args.push('--allowedTools', 'mcp__permissions__approval_prompt');
args.push('--permission-prompt-tool', 'mcp__permissions__approval_prompt');
}else{
args.push('--dangerously-skip-permissions')
}
// Add model selection if not using default
if (this._selectedModel && this._selectedModel !== 'default') {
args.push('--model', this._selectedModel);
@@ -970,6 +983,129 @@ class ClaudeChatProvider {
}
}
private async _initializeMCPConfig(): Promise<void> {
try {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) {return;}
// Create MCP config directory
const mcpConfigDir = path.join(storagePath, 'mcp');
try {
await vscode.workspace.fs.stat(vscode.Uri.file(mcpConfigDir));
} catch {
await vscode.workspace.fs.createDirectory(vscode.Uri.file(mcpConfigDir));
console.log(`Created MCP config directory at: ${mcpConfigDir}`);
}
// Create mcp-servers.json with correct path to compiled MCP permissions server
const mcpConfigPath = path.join(mcpConfigDir, 'mcp-servers.json');
const mcpPermissionsPath = path.join(this._extensionUri.fsPath, 'out', 'permissions', 'mcp-permissions.js');
const permissionRequestsPath = path.join(storagePath, 'permission-requests');
const mcpConfig = {
mcpServers: {
permissions: {
command: 'node',
args: [mcpPermissionsPath],
env: {
CLAUDE_PERMISSIONS_PATH: permissionRequestsPath
}
}
}
};
const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2));
await vscode.workspace.fs.writeFile(vscode.Uri.file(mcpConfigPath), configContent);
console.log(`Created MCP config at: ${mcpConfigPath}`);
} catch (error: any) {
console.error('Failed to initialize MCP config:', error.message);
}
}
private async _initializePermissions(): Promise<void> {
try {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) {return;}
// Create permission requests directory
this._permissionRequestsPath = path.join(storagePath, 'permission-requests');
try {
await vscode.workspace.fs.stat(vscode.Uri.file(this._permissionRequestsPath));
} catch {
await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._permissionRequestsPath));
console.log(`Created permission requests directory at: ${this._permissionRequestsPath}`);
}
// Set up file watcher for *.request files
this._permissionWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this._permissionRequestsPath, '*.request')
);
this._permissionWatcher.onDidCreate(async (uri) => {
// Only handle file scheme URIs, ignore vscode-userdata scheme
if (uri.scheme === 'file') {
await this._handlePermissionRequest(uri);
}
});
this._disposables.push(this._permissionWatcher);
} catch (error: any) {
console.error('Failed to initialize permissions:', error.message);
}
}
private async _handlePermissionRequest(requestUri: vscode.Uri): Promise<void> {
try {
// Read the request file
const content = await vscode.workspace.fs.readFile(requestUri);
const request = JSON.parse(new TextDecoder().decode(content));
// Show permission dialog
const approved = await this._showPermissionDialog(request);
// Write response file
const responseFile = requestUri.fsPath.replace('.request', '.response');
const response = {
id: request.id,
approved: approved,
timestamp: new Date().toISOString()
};
const responseContent = new TextEncoder().encode(JSON.stringify(response));
await vscode.workspace.fs.writeFile(vscode.Uri.file(responseFile), responseContent);
// Clean up request file
await vscode.workspace.fs.delete(requestUri);
} catch (error: any) {
console.error('Failed to handle permission request:', error.message);
}
}
private async _showPermissionDialog(request: any): Promise<boolean> {
const toolName = request.tool || 'Unknown Tool';
const toolInput = JSON.stringify(request.input, null, 2);
const message = `Tool "${toolName}" is requesting permission to execute:\n\n${toolInput}\n\nDo you want to allow this?`;
const result = await vscode.window.showWarningMessage(
message,
{ modal: true },
'Allow',
'Deny'
);
return result === 'Allow';
}
public getMCPConfigPath(): string | undefined {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) {return undefined;}
return path.join(storagePath, 'mcp', 'mcp-servers.json');
}
private _sendAndSaveMessage(message: { type: string, data: any }): void {
// Initialize conversation if this is the first message
if (this._currentConversation.length === 0) {

View File

@@ -0,0 +1,118 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
const server = new McpServer({
name: "Claude Code Permissions MCP Server",
version: "0.0.1",
});
// Get permissions directory from environment
const PERMISSIONS_PATH = process.env.CLAUDE_PERMISSIONS_PATH;
if (!PERMISSIONS_PATH) {
console.error("CLAUDE_PERMISSIONS_PATH environment variable not set");
process.exit(1);
}
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
async function requestPermission(tool_name: string, input: any): Promise<boolean> {
if (!PERMISSIONS_PATH) {
console.error("Permissions path not available");
return false;
}
const requestId = generateRequestId();
const requestFile = path.join(PERMISSIONS_PATH, `${requestId}.request`);
const responseFile = path.join(PERMISSIONS_PATH, `${requestId}.response`);
// Write request file
const request = {
id: requestId,
tool: tool_name,
input: input,
timestamp: new Date().toISOString()
};
try {
fs.writeFileSync(requestFile, JSON.stringify(request, null, 2));
// Poll for response file
const maxWaitTime = 30000; // 30 seconds timeout
const pollInterval = 100; // Check every 100ms
let waitTime = 0;
while (waitTime < maxWaitTime) {
if (fs.existsSync(responseFile)) {
const responseContent = fs.readFileSync(responseFile, 'utf8');
const response = JSON.parse(responseContent);
// Clean up response file
fs.unlinkSync(responseFile);
return response.approved;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
waitTime += pollInterval;
}
// Timeout - clean up request file and deny
if (fs.existsSync(requestFile)) {
fs.unlinkSync(requestFile);
}
console.error(`Permission request ${requestId} timed out`);
return false;
} catch (error) {
console.error(`Error requesting permission: ${error}`);
return false;
}
}
server.tool(
"approval_prompt",
'Request user permission to execute a tool via VS Code dialog',
{
tool_name: z.string().describe("The name of the tool requesting permission"),
input: z.object({}).passthrough().describe("The input for the tool"),
tool_use_id: z.string().optional().describe("The unique tool use request ID"),
},
async ({ tool_name, input }) => {
console.error(`Requesting permission for tool: ${tool_name}`);
const approved = await requestPermission(tool_name, input);
const behavior = approved ? "allow" : "deny";
console.error(`Permission ${behavior}ed for tool: ${tool_name}`);
return {
content: [
{
type: "text",
text: JSON.stringify({
behavior: behavior,
updatedInput: input,
}),
},
],
};
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`Permissions MCP Server running on stdio`);
console.error(`Using permissions directory: ${PERMISSIONS_PATH}`);
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});

View File

@@ -0,0 +1,10 @@
{
"mcpServers": {
"permissions": {
"command": "node",
"args": [
"./out/permissions/mcp-permissions.js"
]
}
}
}