UNPKG

@pluggedin/pluggedin-mcp-proxy

Version:

Unified MCP proxy that aggregates all your MCP servers (STDIO, SSE, Streamable HTTP) into one powerful interface. Access any tool through a single connection, search across unified documents with built-in RAG, and receive notifications from any model. Tes

311 lines (310 loc) 12.2 kB
/** * Custom Instructions Utilities * Helper functions for extracting and managing custom instructions */ /** * Processes custom instructions in a single pass to extract both formatted context and constraints */ export function processInstructions(serverName, serverUuid, messages) { if (!messages || messages.length === 0) { return null; } // Extract raw instruction text from messages let rawInstructions = ''; messages.forEach(msg => { const text = typeof msg.content === 'string' ? msg.content : msg.content.map(c => c.text).join('\n'); rawInstructions += (rawInstructions ? '\n' : '') + text; }); if (!rawInstructions) { return null; } const constraints = {}; const lowerInstructions = rawInstructions.toLowerCase(); // Parse constraints from instructions // Check for read-only constraint if (lowerInstructions.includes('read-only') || lowerInstructions.includes('readonly')) { constraints.readonly = true; } // Check for no-writes constraint if (lowerInstructions.includes('no write') || lowerInstructions.includes('no mutation')) { constraints.noWrites = true; } // Check for no-delete constraint if (lowerInstructions.includes('no delete') || lowerInstructions.includes('no deletion')) { constraints.noDeletes = true; } // Check for no-update constraint if (lowerInstructions.includes('no update') || lowerInstructions.includes('no modification')) { constraints.noUpdates = true; } // Extract rate limits const rateLimitMatch = rawInstructions.match(/(\d+)\s*requests?\s*per\s*(second|minute|hour)/i); if (rateLimitMatch) { constraints.rateLimit = { count: parseInt(rateLimitMatch[1], 10), unit: rateLimitMatch[2].toLowerCase() }; } // Extract allowed operations if specified const allowedMatch = rawInstructions.match(/allowed\s*operations?:\s*([^\n]+)/i); if (allowedMatch) { constraints.allowedOperations = allowedMatch[1] .split(/[,;]/) .map(op => op.trim().toLowerCase()) .filter(Boolean); } // Extract denied operations if specified const deniedMatch = rawInstructions.match(/(?:denied|forbidden|prohibited)\s*operations?:\s*([^\n]+)/i); if (deniedMatch) { constraints.deniedOperations = deniedMatch[1] .split(/[,;]/) .map(op => op.trim().toLowerCase()) .filter(Boolean); } // Format the instructions for display let formattedContext = `### Server Context: ${serverName}\n\n`; // Add the instructions const lines = rawInstructions.split('\n').map(line => line.trim()).filter(Boolean); // If already has markdown headers, preserve them if (lines.some(line => line.startsWith('#'))) { formattedContext += rawInstructions; } else { // Format as a bulleted list for better readability formattedContext += lines.map(line => `- ${line}`).join('\n'); } // Add parsed constraints summary if any exist if (Object.keys(constraints).length > 0) { formattedContext += '\n\n**Constraints:**\n'; if (constraints.readonly) formattedContext += '- Read-only access\n'; if (constraints.noWrites) formattedContext += '- No write operations\n'; if (constraints.noDeletes) formattedContext += '- No delete operations\n'; if (constraints.noUpdates) formattedContext += '- No update operations\n'; if (constraints.rateLimit) { formattedContext += `- Rate limit: ${constraints.rateLimit.count} requests per ${constraints.rateLimit.unit}\n`; } if (constraints.allowedOperations) { formattedContext += `- Allowed operations: ${constraints.allowedOperations.join(', ')}\n`; } if (constraints.deniedOperations) { formattedContext += `- Denied operations: ${constraints.deniedOperations.join(', ')}\n`; } } return { formattedContext, constraints, rawInstructions, serverUuid, serverName }; } /** * Extract custom instructions from server data */ export function extractCustomInstructions(serverData) { if (!serverData.customInstructions) { return null; } // Handle both array and single instruction formats if (Array.isArray(serverData.customInstructions)) { // Check if it's already in McpMessage format (has role and content) if (serverData.customInstructions.length > 0 && typeof serverData.customInstructions[0] === 'object' && 'role' in serverData.customInstructions[0] && 'content' in serverData.customInstructions[0]) { return serverData.customInstructions; } // If it's a simple string array, convert each string to McpMessage format if (serverData.customInstructions.every((item) => typeof item === 'string')) { return serverData.customInstructions.map((text) => ({ role: "user", content: [{ type: "text", text }] })); } return serverData.customInstructions; } // If it's a string, convert to message format if (typeof serverData.customInstructions === 'string') { return [{ role: "user", content: [{ type: "text", text: serverData.customInstructions }] }]; } return null; } /** * Validates a tool invocation against constraints using server UUID lookup */ export function validateToolAgainstConstraints(toolName, serverUuid, constraintMap) { const constraints = constraintMap.get(serverUuid); if (!constraints) { return { valid: true }; // No constraints means allowed } const lowerToolName = toolName.toLowerCase(); // Check allowed operations first (whitelist) if (constraints.allowedOperations && constraints.allowedOperations.length > 0) { const isAllowed = constraints.allowedOperations.some(op => lowerToolName.includes(op)); if (!isAllowed) { return { valid: false, reason: `This operation is not in the allowed list: ${constraints.allowedOperations.join(', ')}` }; } } // Check denied operations (blacklist) if (constraints.deniedOperations && constraints.deniedOperations.length > 0) { const isDenied = constraints.deniedOperations.some(op => lowerToolName.includes(op)); if (isDenied) { return { valid: false, reason: `This operation is explicitly denied for this server` }; } } // Check read-only constraint if (constraints.readonly) { const readPatterns = ['select', 'fetch', 'get', 'read', 'list', 'search', 'find', 'check', 'describe', 'view', 'show', 'inspect', 'browse', 'query', 'scan']; const writePatterns = ['write', 'update', 'delete', 'create', 'insert', 'modify', 'alter', 'drop', 'truncate', 'execute', 'commit', 'rollback', 'put', 'post', 'patch']; // If it explicitly has a read pattern, it's likely safe const hasReadPattern = readPatterns.some(pattern => lowerToolName.includes(pattern)); // If it has a write pattern, it's not allowed const hasWritePattern = writePatterns.some(pattern => lowerToolName.includes(pattern)); if (hasWritePattern && !hasReadPattern) { return { valid: false, reason: 'This server is configured as read-only. Write operations are not allowed.' }; } } // Check specific constraints if (constraints.noWrites) { const writePatterns = ['write', 'create', 'insert', 'add', 'put', 'post']; const hasWritePattern = writePatterns.some(pattern => lowerToolName.includes(pattern)); if (hasWritePattern) { return { valid: false, reason: 'Write operations are not allowed for this server.' }; } } if (constraints.noDeletes) { const deletePatterns = ['delete', 'remove', 'drop', 'destroy', 'purge']; const hasDeletePattern = deletePatterns.some(pattern => lowerToolName.includes(pattern)); if (hasDeletePattern) { return { valid: false, reason: 'Delete operations are not allowed for this server.' }; } } if (constraints.noUpdates) { const updatePatterns = ['update', 'modify', 'alter', 'patch', 'edit', 'change']; const hasUpdatePattern = updatePatterns.some(pattern => lowerToolName.includes(pattern)); if (hasUpdatePattern) { return { valid: false, reason: 'Update operations are not allowed for this server.' }; } } // Rate limiting would need to be implemented with actual tracking // For now, we just acknowledge the constraint exists return { valid: true }; } /** * Creates a constraint map for efficient lookup by server UUID */ export function buildConstraintMap(serverContexts) { const map = new Map(); for (const [uuid, context] of serverContexts.entries()) { if (context.constraints) { map.set(uuid, context.constraints); } } return map; } /** * Build server contexts map from server data (returns UUID-keyed map) */ export function buildServerContextsMap(servers) { const contexts = new Map(); servers.forEach(server => { const instructions = extractCustomInstructions(server); if (!instructions) { return; } const processedContext = processInstructions(server.name || server.uuid, server.uuid, instructions); if (processedContext) { contexts.set(server.uuid, processedContext); } }); return contexts; } /** * Convert ProcessedServerContext to legacy ServerContext format */ export function toLegacyServerContext(processed) { const constraints = []; if (processed.constraints.readonly) constraints.push('read-only'); if (processed.constraints.noWrites) constraints.push('no-write'); if (processed.constraints.noDeletes) constraints.push('no-delete'); if (processed.constraints.noUpdates) constraints.push('no-update'); return { instructions: processed.formattedContext, serverId: processed.serverUuid, isReadOnly: processed.constraints.readonly, constraints }; } /** * Helper to format server instructions for discovery response */ export function formatServerInstructionsForDiscovery(contexts) { if (contexts.size === 0) return ''; const sections = []; for (const context of contexts.values()) { sections.push(context.formattedContext); } return sections.join('\n\n---\n\n'); } /** * Format custom instructions for discovery output * This function fetches MCP servers and formats their custom instructions * for display in the discovery response */ export async function formatCustomInstructionsForDiscovery() { try { // Dynamic import to avoid circular dependency const { getMcpServers } = await import('../fetch-pluggedinmcp.js'); const serverDict = await getMcpServers(); const servers = Object.values(serverDict); const serverContexts = buildServerContextsMap(servers); if (serverContexts.size === 0) { return ''; } let output = '\n## 🔧 Server Custom Instructions (Auto-Injected)\n'; output += 'The following custom instructions are automatically provided to AI assistants:\n\n'; for (const [uuid, context] of serverContexts.entries()) { output += `### ${context.serverName}\n`; output += `**Instructions:** ${context.rawInstructions}\n\n`; } return output; } catch (error) { // Silently skip custom instructions if there's an error return ''; } }