cui-server
Version:
Web UI Agent Platform based on Claude Code
176 lines • 8.1 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import fetch from 'node-fetch';
import { logger } from '../services/logger.js';
// Get CUI server URL from environment
const CUI_SERVER_URL = process.env.CUI_SERVER_URL || `http://localhost:${process.env.CUI_SERVER_PORT || '3001'}`;
// Get CUI streaming ID from environment (passed by ClaudeProcessManager)
const CUI_STREAMING_ID = process.env.CUI_STREAMING_ID;
// Create MCP server
const server = new Server({
name: 'cui-permissions',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Define the approval_prompt tool
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'approval_prompt',
description: 'Request approval for tool usage from CUI',
inputSchema: {
type: 'object',
properties: {
tool_name: {
type: 'string',
description: 'The tool requesting permission',
},
input: {
type: 'object',
description: 'The input for the tool',
},
},
required: ['tool_name', 'input'],
},
}],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'approval_prompt') {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
const { tool_name, input } = request.params.arguments;
try {
// Log the permission request
logger.debug('MCP Permission request received', { tool_name, input, streamingId: CUI_STREAMING_ID });
// Send the permission request to CUI server
const response = await fetch(`${CUI_SERVER_URL}/api/permissions/notify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
toolName: tool_name,
toolInput: input,
streamingId: CUI_STREAMING_ID || 'unknown', // Include the streaming ID from environment
}),
});
if (!response.ok) {
const errorText = await response.text();
logger.error('Failed to notify CUI server', { status: response.status, error: errorText });
throw new Error(`Failed to notify CUI server: ${errorText}`);
}
// Get the permission request ID from the notification response
const notificationData = await response.json();
const permissionRequestId = notificationData.id;
logger.debug('Permission request created', { permissionRequestId, streamingId: CUI_STREAMING_ID });
// Poll for permission decision
const POLL_INTERVAL = 1000; // 1 second
const TIMEOUT = 60 * 60 * 1000; // 1 hour
const startTime = Date.now();
while (true) {
// Check timeout
if (Date.now() - startTime > TIMEOUT) {
logger.warn('Permission request timed out', { tool_name, permissionRequestId });
const timeoutResponse = {
behavior: 'deny',
message: 'Permission request timed out after 10 minutes after user did not respond',
};
return {
content: [{
type: 'text',
text: JSON.stringify(timeoutResponse),
}],
};
}
// Poll for permission status
const pollResponse = await fetch(`${CUI_SERVER_URL}/api/permissions?streamingId=${CUI_STREAMING_ID}&status=pending`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!pollResponse.ok) {
logger.error('Failed to poll permission status', { status: pollResponse.status });
throw new Error(`Failed to poll permission status: ${pollResponse.status}`);
}
const { permissions } = await pollResponse.json();
const permission = permissions.find((p) => p.id === permissionRequestId);
if (!permission) {
// Permission has been processed (no longer pending)
// Fetch all permissions to find our specific one
const allPermissionsResponse = await fetch(`${CUI_SERVER_URL}/api/permissions?streamingId=${CUI_STREAMING_ID}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!allPermissionsResponse.ok) {
logger.error('Failed to fetch all permissions', { status: allPermissionsResponse.status });
throw new Error(`Failed to fetch all permissions: ${allPermissionsResponse.status}`);
}
const { permissions: allPermissions } = await allPermissionsResponse.json();
const processedPermission = allPermissions.find((p) => p.id === permissionRequestId);
if (processedPermission) {
if (processedPermission.status === 'approved') {
logger.debug('Permission approved', { tool_name, permissionRequestId });
const approvalResponse = {
behavior: 'allow',
updatedInput: processedPermission.modifiedInput || input,
};
return {
content: [{
type: 'text',
text: JSON.stringify(approvalResponse),
}],
};
}
else if (processedPermission.status === 'denied') {
logger.debug('Permission denied', { tool_name, permissionRequestId });
const denyResponse = {
behavior: 'deny',
message: processedPermission.denyReason || 'The user doesnt want to proceed with this tool use.The tool use was rejected(eg.if it was a file edit, the new_string was NOT written to the file).STOP what you are doing and wait for the user to tell you how to proceed.',
};
return {
content: [{
type: 'text',
text: JSON.stringify(denyResponse),
}],
};
}
}
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
}
}
catch (error) {
logger.error('Error processing permission request', { error });
// Return a deny response on error
const denyResponse = {
behavior: 'deny',
message: `Permission denied due to error: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
return {
content: [{
type: 'text',
text: JSON.stringify(denyResponse),
}],
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('MCP Permission server started', { cuiServerUrl: CUI_SERVER_URL });
}
main().catch((error) => {
logger.error('MCP server error', { error });
process.exit(1);
});
//# sourceMappingURL=index.js.map