mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-09 02:59:43 +00:00
Add claude-code-chat-permissions-mcp folder
This commit is contained in:
@@ -11,3 +11,4 @@ vsc-extension-quickstart.md
|
||||
**/.vscode-test.*
|
||||
backup
|
||||
.claude
|
||||
claude-code-chat-permissions-mcp/**
|
||||
13954
claude-code-chat-permissions-mcp/mcp-permissions.js
Normal file
13954
claude-code-chat-permissions-mcp/mcp-permissions.js
Normal file
File diff suppressed because one or more lines are too long
212
claude-code-chat-permissions-mcp/mcp-permissions.ts
Normal file
212
claude-code-chat-permissions-mcp/mcp-permissions.ts
Normal 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);
|
||||
});
|
||||
1086
claude-code-chat-permissions-mcp/package-lock.json
generated
Normal file
1086
claude-code-chat-permissions-mcp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
claude-code-chat-permissions-mcp/package.json
Normal file
21
claude-code-chat-permissions-mcp/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
11
claude-code-chat-permissions-mcp/tsconfig.json
Normal file
11
claude-code-chat-permissions-mcp/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"lib": ["es2015"]
|
||||
}
|
||||
49
src/ui.ts
49
src/ui.ts
@@ -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>`;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
},
|
||||
"exclude": [
|
||||
"mcp-permissions.js"
|
||||
"mcp-permissions.js",
|
||||
"claude-code-chat-permissions-mcp"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user