Skip to main content
Glama

Revenue Engine MCP

index.js28.2 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // Your Apps Script Web App URL - Version 26 with matrixDailySummary and matrixTimeAnalysis const API_URL = "https://script.google.com/macros/s/AKfycbxtCxJR1Kavp_QqeljIOtSvrDcBFhW4xUl0XaA4I9ILGjAkX-fcv4ySSCvYzIclb4mGhw/exec"; // Debug logging to file const LOG_FILE = 'C:\\Users\\Node1\\revenue-engine-mcp\\debug.log'; function debugLog(message) { try { const timestamp = new Date().toISOString(); fs.appendFileSync(LOG_FILE, `[${timestamp}] ${message}\n`); } catch (e) { console.error('Failed to write to log file:', e); } } // CONFIGURATION FOR FILE SYSTEM AND CLI TOOLS const ALLOWED_PATHS = [ 'C:\\Users\\Node1\\revenue-engine-mcp', 'C:\\Users\\Node1\\Documents\\revenue-engine\\mcp-server', 'C:\\Users\\Node1\\Documents\\revenue-engine\\apps-script', ]; const ALLOWED_COMMANDS = { 'clasp push': 'Sync local files to Google Apps Script', 'clasp pull': 'Sync Google Apps Script to local files', 'clasp deploy': 'Deploy Apps Script as web app', 'clasp status': 'Check sync status', 'clasp list': 'List Apps Script projects', 'npm install': 'Install node packages', 'git status': 'Check git status', 'git add': 'Stage changes', 'git commit': 'Commit changes', 'git init': 'Initialize git repository', 'git remote': 'Manage remote repositories', 'git push': 'Push to remote', 'git branch': 'Manage branches', 'mkdir': 'Create directory', 'md': 'Create directory (Windows)', 'dir': 'List directory contents', 'ls': 'List directory contents', 'del': 'Delete files (Windows)', 'rm': 'Remove files (Unix)', }; // Helper to check if path is allowed function isPathAllowed(filePath) { const normalized = filePath.replace(/\//g, '\\'); return ALLOWED_PATHS.some(allowedPath => normalized.startsWith(allowedPath) ); } // Helper to check if command is allowed function isCommandAllowed(command) { const cmd = command.trim(); return Object.keys(ALLOWED_COMMANDS).some(allowed => cmd.startsWith(allowed) ); } // Helper function to call the API async function callAPI(action, data = {}) { debugLog('=== API CALL START ==='); debugLog(`Action: ${action}`); debugLog(`Data: ${JSON.stringify(data)}`); try { // Build form-encoded body for POST const formData = new URLSearchParams(); formData.append('action', action); // Add all data fields to form for (const [key, value] of Object.entries(data)) { if (value !== undefined && value !== null) { formData.append(key, value.toString()); } } const formString = formData.toString(); debugLog(`FormData: ${formString}`); debugLog(`API_URL: ${API_URL}`); // Use POST with proper content type const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formString }); debugLog(`Response status: ${response.status}`); debugLog(`Response ok: ${response.ok}`); if (!response.ok) { debugLog(`Response not OK: ${response.status} ${response.statusText}`); throw new Error(`API request failed: ${response.status} ${response.statusText}`); } const text = await response.text(); debugLog(`Response text length: ${text.length}`); debugLog(`Response text: ${text}`); if (!text) { debugLog('ERROR: Empty response from API'); throw new Error('Empty response from API'); } const parsed = JSON.parse(text); debugLog(`Parsed successfully: ${JSON.stringify(parsed)}`); debugLog('=== API CALL END ==='); return parsed; } catch (error) { debugLog(`ERROR in callAPI: ${error.message}`); debugLog(`ERROR stack: ${error.stack}`); throw error; } } // Create MCP server const server = new Server( { name: "revenue-engine", version: "1.7.0", }, { capabilities: { tools: {}, }, } ); // Define all tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_dashboard", description: "Get current revenue dashboard with all key metrics", inputSchema: { type: "object", properties: {}, }, }, { name: "get_pipeline", description: "Get all leads in the pipeline", inputSchema: { type: "object", properties: {}, }, }, { name: "get_upcoming_meetings", description: "Get upcoming meetings from Google Calendar (next 7 days)", inputSchema: { type: "object", properties: {}, }, }, { name: "add_lead", description: "Add a new lead to the pipeline", inputSchema: { type: "object", properties: { companyName: { type: "string", description: "Company name" }, contactName: { type: "string", description: "Contact person name" }, contactEmail: { type: "string", description: "Contact email" }, contactPhone: { type: "string", description: "Contact phone" }, industry: { type: "string", description: "Industry/sector" }, source: { type: "string", description: "Lead source", enum: ["Upwork", "LinkedIn", "Cold Email", "Referral", "Website", "Other"] }, estimatedValue: { type: "number", description: "Estimated project value" }, servicesInterestedIn: { type: "string", description: "Services interested in" }, notes: { type: "string", description: "Additional notes" }, }, required: ["companyName"], }, }, { name: "update_lead", description: "Update an existing lead", inputSchema: { type: "object", properties: { leadId: { type: "number", description: "Lead ID to update" }, status: { type: "string", enum: ["New", "Contacted", "Call Booked", "Proposal Sent", "Closed", "Lost"] }, estimatedValue: { type: "number" }, notes: { type: "string" }, nextAction: { type: "string" }, nextActionDate: { type: "string", description: "YYYY-MM-DD" }, }, required: ["leadId"], }, }, { name: "log_outreach", description: "Log an outreach activity", inputSchema: { type: "object", properties: { leadId: { type: "number" }, companyName: { type: "string" }, channel: { type: "string", enum: ["Cold Email", "LinkedIn", "Upwork", "Phone", "Gmail", "Other"] }, templateUsed: { type: "string" }, templateId: { type: "number" }, responseReceived: { type: "string", enum: ["Yes", "No"] }, responseType: { type: "string", enum: ["Interested", "Not Interested", "Question", "Meeting Booked", "No Response"] }, notes: { type: "string" }, }, required: ["companyName", "channel"], }, }, { name: "add_revenue", description: "Add a closed deal", inputSchema: { type: "object", properties: { clientName: { type: "string" }, leadId: { type: "number" }, serviceType: { type: "string", enum: ["Website", "Automation", "SaaS Consulting", "Combined", "Other"] }, projectDescription: { type: "string" }, contractValue: { type: "number" }, status: { type: "string", enum: ["Proposed", "Accepted", "In Progress", "Delivered", "Paid"] }, paymentReceived: { type: "number" }, notes: { type: "string" }, }, required: ["clientName", "contractValue"], }, }, { name: "add_task", description: "Add a new task", inputSchema: { type: "object", properties: { taskDescription: { type: "string" }, priority: { type: "string", enum: ["High", "Medium", "Low"] }, dueDate: { type: "string", description: "YYYY-MM-DD" }, relatedTo: { type: "string" }, estimatedHours: { type: "number" }, notes: { type: "string" }, }, required: ["taskDescription"], }, }, { name: "get_tasks", description: "Get all tasks", inputSchema: { type: "object", properties: {}, }, }, { name: "update_task", description: "Update a task", inputSchema: { type: "object", properties: { taskId: { type: "number" }, status: { type: "string", enum: ["To Do", "In Progress", "Completed", "Blocked"] }, actualHours: { type: "number" }, notes: { type: "string" }, }, required: ["taskId"], }, }, { name: "get_templates", description: "Get message templates with performance metrics", inputSchema: { type: "object", properties: {}, }, }, { name: "log_daily_metrics", description: "Log daily activity metrics", inputSchema: { type: "object", properties: { date: { type: "string", description: "YYYY-MM-DD" }, outreachAttempts: { type: "number" }, responses: { type: "number" }, callsBooked: { type: "number" }, proposalsSent: { type: "number" }, dealsClosed: { type: "number" }, revenueClosed: { type: "number" }, }, }, }, { name: "get_metrics", description: "Get recent daily metrics (last 7 days)", inputSchema: { type: "object", properties: {}, }, }, { name: "search_gmail", description: "Search Gmail inbox. Use Gmail search syntax like 'from:email@example.com' or 'is:unread' or 'subject:proposal'", inputSchema: { type: "object", properties: { query: { type: "string", description: "Gmail search query (default: 'is:unread')" }, maxResults: { type: "number", description: "Max results (default: 25, max: 100)" } } } }, { name: "get_email_content", description: "Get full content of an email thread by ID. Returns complete email body, attachments info, and all messages in the thread.", inputSchema: { type: "object", properties: { threadId: { type: "string", description: "Gmail thread ID (get this from search_gmail results)" } }, required: ["threadId"] } }, { name: "send_email", description: "Send an email via Gmail. ALWAYS get user approval before calling this.", inputSchema: { type: "object", properties: { to: { type: "string", description: "Recipient email address" }, subject: { type: "string", description: "Email subject line" }, body: { type: "string", description: "Email body content" } }, required: ["to", "subject", "body"] } }, { name: "check_new_leads", description: "Check for new leads added in last 24 hours that need welcome emails", inputSchema: { type: "object", properties: {} } }, // MATRIX KNOWLEDGE BASE TOOLS { name: "setup_matrix_sheet", description: "Auto-create Knowledge Matrix sheet with proper structure and headers. Run this once before using other Matrix tools.", inputSchema: { type: "object", properties: {} } }, { name: "write_matrix_entry", description: "Write or append entry to Knowledge Matrix. Topics: Bugs & Fixes, Features Added, Testing Results, Decisions & Direction, Documentation Updates, Next Session Goals", inputSchema: { type: "object", properties: { date: { type: "string", description: "Date in YYYY-MM-DD format (defaults to today)" }, topic: { type: "string", description: "Matrix topic", enum: ["Bugs & Fixes", "Features Added", "Testing Results", "Decisions & Direction", "Documentation Updates", "Next Session Goals"] }, content: { type: "string", description: "Entry content with timestamp (e.g., '3:45pm CST 🐛 Fixed bug in addTask')" } }, required: ["topic", "content"] } }, { name: "read_matrix_snapshot", description: "Read Matrix entries for a date range. Returns all entries for specified topics and dates.", inputSchema: { type: "object", properties: { startDate: { type: "string", description: "Start date YYYY-MM-DD (optional, defaults to beginning)" }, endDate: { type: "string", description: "End date YYYY-MM-DD (optional, defaults to today)" }, topics: { type: "array", items: { type: "string" }, description: "Array of topics to include (optional, defaults to all)" } } } }, { name: "get_matrix_row", description: "Get all topics for a specific date. Returns complete row from Matrix.", inputSchema: { type: "object", properties: { date: { type: "string", description: "Date in YYYY-MM-DD format (defaults to today)" } } } }, { name: "query_matrix", description: "Search Matrix for keyword across topics and dates", inputSchema: { type: "object", properties: { keyword: { type: "string", description: "Search term" }, topics: { type: "array", items: { type: "string" }, description: "Topics to search (optional, defaults to all)" }, limit: { type: "number", description: "Max results (default 50)" } }, required: ["keyword"] } }, { name: "delete_matrix_rows", description: "Delete rows from Knowledge Matrix with confirmation requirement. First call shows preview, second call with confirm=true executes deletion. Cannot delete header rows (1-2).", inputSchema: { type: "object", properties: { startRow: { type: "number", description: "First row to delete (must be >= 3)" }, endRow: { type: "number", description: "Last row to delete (inclusive)" }, confirm: { type: "boolean", description: "Set to true to actually delete (first call without this shows preview)" } }, required: ["startRow", "endRow"] } }, { name: "matrix_daily_summary", description: "Generate formatted summary of Matrix entries for a specific date. Automatically parses timestamps, bug UIDs, time spent, and generates human-readable output.", inputSchema: { type: "object", properties: { date: { type: "string", description: "Date in YYYY-MM-DD format (defaults to today)" }, format: { type: "string", description: "Output format", enum: ["bullet", "prose", "slack"] } } } }, { name: "matrix_time_analysis", description: "Analyze time spent across Matrix entries. Tracks total time by topic, bug UID, or week. Parses time markers like [30m], [2h] from entries.", inputSchema: { type: "object", properties: { startDate: { type: "string", description: "Start date YYYY-MM-DD (optional, defaults to beginning)" }, endDate: { type: "string", description: "End date YYYY-MM-DD (optional, defaults to today)" }, groupBy: { type: "string", description: "How to group the analysis", enum: ["topic", "bug", "week", "day"] } } } }, // FILE SYSTEM TOOLS { name: "read_file", description: "Read contents of a file. Only works in allowed directories: revenue-engine-mcp, apps-script folders", inputSchema: { type: "object", properties: { path: { type: "string", description: "Full file path (e.g., C:\\Users\\Node1\\revenue-engine-mcp\\index.js)" } }, required: ["path"] } }, { name: "edit_file", description: "Surgically edit a file by finding and replacing exact text. 50% more efficient than read+write. Use after read_file to ensure exact match. Errors if text not found or appears multiple times. Creates backup automatically.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Full file path" }, find: { type: "string", description: "Exact text to find (must match exactly including whitespace)" }, replace_with: { type: "string", description: "Text to replace it with" } }, required: ["path", "find", "replace_with"] } }, { name: "write_file", description: "Write or update entire file. Creates backup automatically. Use for complex multi-location edits or new files. Only works in allowed directories.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Full file path" }, content: { type: "string", description: "File content to write" } }, required: ["path", "content"] } }, { name: "run_command", description: "Execute allowed shell commands (clasp, npm, git, dir). For clasp commands, run from apps-script folder.", inputSchema: { type: "object", properties: { command: { type: "string", description: "Command to run (must be in allowed list)" }, workingDirectory: { type: "string", description: "Directory to run command in (optional, defaults to revenue-engine-mcp)" } }, required: ["command"] } } ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result; switch (name) { // Existing tools case "get_dashboard": result = await callAPI("getDashboard"); break; case "get_pipeline": result = await callAPI("getPipeline"); break; case "get_upcoming_meetings": result = await callAPI("getUpcomingMeetings"); break; case "add_lead": result = await callAPI("addLead", args); break; case "update_lead": result = await callAPI("updateLead", args); break; case "log_outreach": result = await callAPI("logOutreach", args); break; case "add_revenue": result = await callAPI("addRevenue", args); break; case "add_task": result = await callAPI("addTask", args); break; case "get_tasks": result = await callAPI("getTasks"); break; case "update_task": result = await callAPI("updateTask", args); break; case "get_templates": result = await callAPI("getTemplates"); break; case "log_daily_metrics": result = await callAPI("logMetric", args); break; case "get_metrics": result = await callAPI("getMetrics"); break; case "search_gmail": result = await callAPI("searchGmail", args); break; case "get_email_content": result = await callAPI("getEmailContent", args); break; case "send_email": result = await callAPI("sendEmail", args); break; case "check_new_leads": result = await callAPI("checkNewLeads"); break; // MATRIX KNOWLEDGE BASE TOOLS case "setup_matrix_sheet": result = await callAPI("setupMatrixSheet"); break; case "write_matrix_entry": result = await callAPI("writeMatrixEntry", args); break; case "read_matrix_snapshot": result = await callAPI("readMatrixSnapshot", args); break; case "get_matrix_row": result = await callAPI("getMatrixRow", args); break; case "query_matrix": result = await callAPI("queryMatrix", args); break; case "delete_matrix_rows": result = await callAPI("deleteMatrixRows", args); break; case "matrix_daily_summary": result = await callAPI("matrixDailySummary", args); break; case "matrix_time_analysis": result = await callAPI("matrixTimeAnalysis", args); break; // FILE SYSTEM TOOLS case "read_file": { const { path } = args; if (!isPathAllowed(path)) { throw new Error(`Access denied: Path not in allowed directories`); } if (!fs.existsSync(path)) { throw new Error(`File not found: ${path}`); } const content = fs.readFileSync(path, 'utf8'); result = { success: true, path: path, content: content, size: content.length }; break; } case "edit_file": { const { path, find, replace_with } = args; if (!isPathAllowed(path)) { throw new Error(`Access denied: Path not in allowed directories`); } if (!fs.existsSync(path)) { throw new Error(`File not found: ${path}`); } // Read current content const content = fs.readFileSync(path, 'utf8'); const lines = content.split('\n'); // Count occurrences const occurrences = (content.match(new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; if (occurrences === 0) { throw new Error(`Text not found in file. Search text: "${find.substring(0, 100)}..."`); } if (occurrences > 1) { throw new Error(`Text appears ${occurrences} times in file. Must be unique for safety. Search text: "${find.substring(0, 100)}..."`); } // Find the line numbers where the change occurs const findLines = find.split('\n'); const changedLines = []; let foundAtLine = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].includes(findLines[0])) { // Check if this is the full match let isMatch = true; for (let j = 0; j < findLines.length; j++) { if (i + j >= lines.length || !lines[i + j].includes(findLines[j])) { isMatch = false; break; } } if (isMatch) { foundAtLine = i + 1; // 1-indexed for humans for (let j = 0; j < findLines.length; j++) { changedLines.push(i + j + 1); } break; } } } // Create backup const backupPath = `${path}.backup-${Date.now()}`; fs.copyFileSync(path, backupPath); debugLog(`Created backup: ${backupPath}`); // Perform replacement const newContent = content.replace(find, replace_with); fs.writeFileSync(path, newContent, 'utf8'); result = { success: true, path: path, changes: { linesModified: changedLines, totalLines: changedLines.length, startLine: changedLines[0], endLine: changedLines[changedLines.length - 1], before: find.substring(0, 200) + (find.length > 200 ? '...' : ''), after: replace_with.substring(0, 200) + (replace_with.length > 200 ? '...' : ''), }, backupCreated: backupPath, message: `Successfully edited ${changedLines.length} line(s) at lines ${changedLines.join(', ')}` }; break; } case "write_file": { const { path, content } = args; if (!isPathAllowed(path)) { throw new Error(`Access denied: Path not in allowed directories`); } // Create backup if file exists if (fs.existsSync(path)) { const backupPath = `${path}.backup-${Date.now()}`; fs.copyFileSync(path, backupPath); debugLog(`Created backup: ${backupPath}`); } fs.writeFileSync(path, content, 'utf8'); result = { success: true, path: path, bytesWritten: content.length, message: "File written successfully" }; break; } case "run_command": { const { command, workingDirectory } = args; if (!isCommandAllowed(command)) { throw new Error(`Command not allowed: ${command}. Allowed commands: ${Object.keys(ALLOWED_COMMANDS).join(', ')}`); } const cwd = workingDirectory || 'C:\\Users\\Node1\\revenue-engine-mcp'; if (!isPathAllowed(cwd)) { throw new Error(`Working directory not allowed: ${cwd}`); } debugLog(`Running command: ${command} in ${cwd}`); const { stdout, stderr } = await execAsync(command, { cwd }); result = { success: true, command: command, workingDirectory: cwd, stdout: stdout, stderr: stderr }; break; } default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error.message}`, }, ], isError: true, }; } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Revenue Engine MCP Server v1.7.0 running on stdio"); } main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PromptishOperations/mcpSpec'

If you have feedback or need assistance with the MCP directory API, please join our Discord server