Add claude-code-chat-permissions-mcp folder

This commit is contained in:
andrepimenta
2025-07-29 00:58:20 +01:00
parent 5abb1fedd9
commit d6a73a1a7f
8 changed files with 15337 additions and 2 deletions

View File

@@ -11,3 +11,4 @@ vsc-extension-quickstart.md
**/.vscode-test.*
backup
.claude
claude-code-chat-permissions-mcp/**

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,212 @@
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);
}
interface WorkspacePermissions {
alwaysAllow: {
[toolName: string]: boolean | string[]; // true for all, or array of allowed commands/patterns
};
}
function getWorkspacePermissionsPath(): string | null {
if (!PERMISSIONS_PATH) return null;
return path.join(PERMISSIONS_PATH, 'permissions.json');
}
function loadWorkspacePermissions(): WorkspacePermissions {
const permissionsPath = getWorkspacePermissionsPath();
if (!permissionsPath || !fs.existsSync(permissionsPath)) {
return { alwaysAllow: {} };
}
try {
const content = fs.readFileSync(permissionsPath, 'utf8');
return JSON.parse(content);
} catch (error) {
console.error(`Error loading workspace permissions: ${error}`);
return { alwaysAllow: {} };
}
}
function isAlwaysAllowed(toolName: string, input: any): boolean {
const permissions = loadWorkspacePermissions();
const toolPermission = permissions.alwaysAllow[toolName];
if (!toolPermission) return false;
// If it's true, always allow
if (toolPermission === true) return true;
// If it's an array, check for specific commands (mainly for Bash)
if (Array.isArray(toolPermission)) {
if (toolName === 'Bash' && input.command) {
const command = input.command.trim();
return toolPermission.some(allowedCmd => {
// Support exact match or pattern matching
if (allowedCmd.includes('*')) {
// Handle patterns like "npm i *" to match both "npm i" and "npm i something"
const baseCommand = allowedCmd.replace(' *', '');
if (command === baseCommand) {
return true; // Exact match for base command
}
// Pattern match for command with arguments
const pattern = allowedCmd.replace(/\*/g, '.*');
return new RegExp(`^${pattern}$`).test(command);
}
return command.startsWith(allowedCmd);
});
}
}
return false;
}
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
async function requestPermission(tool_name: string, input: any): Promise<{approved: boolean, reason?: string}> {
if (!PERMISSIONS_PATH) {
console.error("Permissions path not available");
return { approved: false, reason: "Permissions path not configured" };
}
// Check if this tool/command is always allowed for this workspace
if (isAlwaysAllowed(tool_name, input)) {
console.error(`Tool ${tool_name} is always allowed for this workspace`);
return { approved: true };
}
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));
// Use fs.watch to wait for response file
return new Promise<{approved: boolean, reason?: string}>((resolve) => {
const timeout = setTimeout(() => {
watcher.close();
// Clean up request file on timeout
if (fs.existsSync(requestFile)) {
fs.unlinkSync(requestFile);
}
console.error(`Permission request ${requestId} timed out`);
resolve({ approved: false, reason: "Permission request timed out" });
}, 3600000); // 1 hour timeout
const watcher = fs.watch(PERMISSIONS_PATH, (eventType, filename) => {
if (eventType === 'rename' && filename === path.basename(responseFile)) {
// Check if file exists (rename event can be for creation or deletion)
if (fs.existsSync(responseFile)) {
try {
const responseContent = fs.readFileSync(responseFile, 'utf8');
const response = JSON.parse(responseContent);
// Clean up response file
fs.unlinkSync(responseFile);
// Clear timeout and close watcher
clearTimeout(timeout);
watcher.close();
resolve({
approved: response.approved,
reason: response.approved ? undefined : "User rejected the request"
});
} catch (error) {
console.error(`Error reading response file: ${error}`);
// Continue watching in case of read error
}
}
}
});
// Handle watcher errors
watcher.on('error', (error) => {
console.error(`File watcher error: ${error}`);
clearTimeout(timeout);
watcher.close();
resolve({ approved: false, reason: "File watcher error" });
});
});
} catch (error) {
console.error(`Error requesting permission: ${error}`);
return { approved: false, reason: `Error processing permission request: ${error}` };
}
}
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 permissionResult = await requestPermission(tool_name, input);
const behavior = permissionResult.approved ? "allow" : "deny";
console.error(`Permission ${behavior}ed for tool: ${tool_name}`);
return {
content: [
{
type: "text",
text: behavior === "allow" ?
JSON.stringify({
behavior: behavior,
updatedInput: input,
})
:
JSON.stringify({
behavior: behavior,
message: permissionResult.reason || "Permission denied",
})
,
},
],
};
}
);
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);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
{
"name": "claude-code-chat-permissions-mcp",
"version": "1.0.0",
"main": "dist/mcp-permissions.js",
"scripts": {
"start": "tsc && node dist/mcp-permissions.js",
"lint": "eslint . --ext .ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@types/node": "^24.0.13",
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",
"zod": "^3.25.76"
}
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}

View File

@@ -1414,6 +1414,8 @@ const html = `<!DOCTYPE html>
function sendMessage() {
const text = messageInput.value.trim();
if (text) {
sendStats('Send message');
vscode.postMessage({
type: 'sendMessage',
text: text,
@@ -1437,6 +1439,11 @@ const html = `<!DOCTYPE html>
function toggleThinkingMode() {
thinkingModeEnabled = !thinkingModeEnabled;
if (thinkingModeEnabled) {
sendStats('Thinking mode enabled');
}
const switchElement = document.getElementById('thinkingModeSwitch');
const toggleLabel = document.getElementById('thinkingModeLabel');
if (thinkingModeEnabled) {
@@ -1461,6 +1468,17 @@ const html = `<!DOCTYPE html>
let requestStartTime = null;
let requestTimer = null;
// Send usage statistics
function sendStats(eventName) {
try {
if (typeof umami !== 'undefined' && umami.track) {
umami.track(eventName);
}
} catch (error) {
console.error('Error sending stats:', error);
}
}
function updateStatus(text, state = 'ready') {
statusTextDiv.textContent = text;
statusDiv.className = \`status \${state}\`;
@@ -1754,6 +1772,8 @@ const html = `<!DOCTYPE html>
}
function enableYoloMode() {
sendStats('YOLO mode enabled');
// Update the checkbox
const yoloModeCheckbox = document.getElementById('yolo-mode');
if (yoloModeCheckbox) {
@@ -1844,6 +1864,8 @@ const html = `<!DOCTYPE html>
}
function saveMCPServer() {
sendStats('MCP server added');
const name = document.getElementById('serverName').value.trim();
const type = document.getElementById('serverType').value;
@@ -2017,6 +2039,8 @@ const html = `<!DOCTYPE html>
}
}
sendStats('MCP server added');
// Add the server
vscode.postMessage({
type: 'saveMCPServer',
@@ -2425,6 +2449,8 @@ const html = `<!DOCTYPE html>
}
function stopRequest() {
sendStats('Stop request');
vscode.postMessage({
type: 'stopRequest'
});
@@ -2582,6 +2608,12 @@ const html = `<!DOCTYPE html>
case 'error':
if (message.data.trim()) {
// Check if this is an install required error
if (message.data.includes('Install claude code first') ||
message.data.includes('command not found') ||
message.data.includes('ENOENT')) {
sendStats('Install required');
}
addMessage(message.data, 'error');
}
updateStatusWithTotals();
@@ -2710,6 +2742,7 @@ const html = `<!DOCTYPE html>
break;
case 'loginRequired':
sendStats('Login required');
addMessage('🔐 Login Required\\n\\nYour Claude API key is invalid or expired.\\nA terminal has been opened - please run the login process there.\\n\\nAfter logging in, come back to this chat to continue.', 'error');
updateStatus('Login Required', 'error');
break;
@@ -2884,6 +2917,8 @@ const html = `<!DOCTYPE html>
}
function enableYoloMode(permissionId) {
sendStats('YOLO mode enabled');
// Hide the menu
document.getElementById(\`permissionMenu-\${permissionId}\`).style.display = 'none';
@@ -2910,6 +2945,8 @@ const html = `<!DOCTYPE html>
// Session management functions
function newSession() {
sendStats('New chat');
vscode.postMessage({
type: 'newSession'
});
@@ -3138,6 +3175,7 @@ const html = `<!DOCTYPE html>
const chatContainer = document.getElementById('chatContainer');
if (historyDiv.style.display === 'none') {
sendStats('History opened');
// Show conversation history
requestConversationList();
historyDiv.style.display = 'block';
@@ -3621,6 +3659,17 @@ const html = `<!DOCTYPE html>
});
</script>
<!--
Analytics FAQ:
1. Is Umami GDPR compliant?
Yes, Umami does not collect any personally identifiable information and anonymizes all data collected. Users cannot be identified and are never tracked across websites.
2. Do I need to display a cookie notice to users?
No, Umami does not use any cookies in the tracking code.
-->
<script defer src="https://cloud.umami.is/script.js" data-website-id="d050ac9b-2b6d-4c67-b4c6-766432f95644"></script>
</body>
</html>`;

View File

@@ -15,6 +15,7 @@
// "noUnusedParameters": true, /* Report errors on unused parameters. */
},
"exclude": [
"mcp-permissions.js"
"mcp-permissions.js",
"claude-code-chat-permissions-mcp"
]
}