index.js•28.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);
});