mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-08 20:29:43 +00:00
212 lines
6.7 KiB
TypeScript
212 lines
6.7 KiB
TypeScript
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);
|
|
}); |