const express = require('express'); const cors = require('cors'); // Dynamic import for node-fetch const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); const app = express(); const PORT = 7860; // 1. CONFIGURATION app.set('trust proxy', 1); app.use(express.json({ limit: '10mb' })); app.use(cors({ origin: '*', methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'MCP-Protocol-Version'] })); // 2. LOGGING app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); }); // --- HELPER: DATA MINIMIZER (Token Saver) --- const minimizeData = (toolName, data) => { if (!data) return "No data"; // A. Optimize Source List (Massive reduction) if (toolName === "list_sources" && data.sources) { return data.sources.map(s => ({ id: s.id, // The only thing the LLM really needs repo: s.githubRepo?.repo || "unknown" })); } // B. Optimize Activities (The biggest offender) if (toolName === "get_activities" && data.activities) { // 1. Take only the last 15 activities (LLM doesn't need history from 3 days ago) const recent = data.activities.slice(-15); return recent.map(act => { let summary = { type: act.originator, // 'agent' or 'user' title: act.progressUpdated?.title || "Update", }; // Extract Plan if (act.planGenerated) { summary.plan = act.planGenerated.plan.steps.map(s => s.title); } // Extract PR Link (Crucial for your second question) if (act.outputs) { const pr = act.outputs.find(o => o.pullRequest); if (pr) summary.pr_link = pr.pullRequest.url; } // HANDLE ARTIFACTS (CRITICAL OPTIMIZATION) // We strip massive bash outputs and git diffs if (act.artifacts) { summary.artifacts = act.artifacts.map(art => { if (art.bashOutput) { return { type: "bash", command: art.bashOutput.command?.trim(), // Truncate output to 200 chars to save tokens output_preview: (art.bashOutput.output || "").substring(0, 200) + "..." }; } if (art.changeSet) { return { type: "code_change", files: "See PR for details", // Don't dump diffs into context baseCommit: art.changeSet.gitPatch?.baseCommitId }; } return "Binary/Image Data (Hidden)"; }); } return summary; }); } return data; }; // --- AUTH MIDDLEWARE --- const authMiddleware = (req, res, next) => { const expected = process.env.MCP_AUTH_TOKEN; if (!expected) return next(); const auth = req.headers['authorization']; if (!auth || !auth.startsWith('Bearer ') || auth.split(' ')[1] !== expected) { return res.status(401).json({ jsonrpc: "2.0", id: null, error: { code: -32000, message: "Unauthorized" } }); } next(); }; // 3. MAIN HANDLER const mcpHandler = async (req, res) => { try { const body = req.body; const { method, params, id } = body; const requestId = id !== undefined ? id : null; const send = (result) => res.json({ jsonrpc: "2.0", id: requestId, result }); const error = (code, msg) => res.json({ jsonrpc: "2.0", id: requestId, error: { code, message: msg } }); // --- HANDLERS --- if (method === "initialize") { return send({ protocolVersion: "2024-11-05", capabilities: { tools: {}, prompts: {}, resources: {} }, serverInfo: { name: "jules-mcp-optimized", version: "1.0.2" } }); } if (method === "notifications/initialized") return send(true); if (method === "ping") return send({}); // --- LIST TOOLS --- if (method === "tools/list") { return send({ tools: [ { name: "list_sources", description: "List GitHub repositories. Returns minimized IDs to save context.", inputSchema: { type: "object", properties: {} } }, { name: "create_session", description: "Start a new task. If auto_pr is true, Jules will publish a PR automatically.", inputSchema: { type: "object", properties: { source: { type: "string", description: "Source ID (e.g. 'owner/repo')" }, prompt: { type: "string", description: "Task description" }, branch: { type: "string", description: "Branch (default: main)" }, auto_pr: { type: "boolean", description: "Set true to have Jules create the PR automatically." } }, required: ["source", "prompt"] } }, { name: "get_activities", description: "Get recent session logs. Returns summarized data (no massive diffs).", inputSchema: { type: "object", properties: { session_id: { type: "string" } }, required: ["session_id"] } }, { name: "send_message", description: "Reply to Jules.", inputSchema: { type: "object", properties: { session_id: { type: "string" }, message: { type: "string" } }, required: ["session_id", "message"] } } ] }); } // --- CALL TOOL --- if (method === "tools/call") { const { name, arguments: args } = params || {}; const apiKey = process.env.JULES_API_KEY; if (!apiKey) return error(-32000, "Server Error: JULES_API_KEY missing"); const callJules = async (ep, m, b) => { const r = await fetch(`https://jules.googleapis.com/v1alpha/${ep}`, { method: m, headers: { "X-Goog-Api-Key": apiKey, "Content-Type": "application/json" }, body: b ? JSON.stringify(b) : undefined }); if (!r.ok) { const txt = await r.text(); throw new Error(`${r.status}: ${txt}`); } const t = await r.text(); return t ? JSON.parse(t) : { success: true }; }; try { let data; if (name === "list_sources") { data = await callJules("sources", "GET"); } else if (name === "create_session") { // Normalize Source let sourceInput = args.source; if (!sourceInput.includes('/')) return error(-32000, "Invalid source. Use 'owner/repo'"); if (!sourceInput.startsWith('sources/')) { sourceInput = sourceInput.startsWith('github/') ? `sources/${sourceInput}` : `sources/github/${sourceInput}`; } data = await callJules("sessions", "POST", { prompt: args.prompt, sourceContext: { source: sourceInput, githubRepoContext: { startingBranch: args.branch || "main" } }, // THIS handles the PR creation automationMode: args.auto_pr ? "AUTO_CREATE_PR" : undefined, title: args.prompt.substring(0, 50) }); } else if (name === "get_activities") { const sid = args.session_id.replace("sessions/", ""); data = await callJules(`sessions/${sid}/activities?pageSize=30`, "GET"); } else if (name === "send_message") { const sid = args.session_id.replace("sessions/", ""); await callJules(`sessions/${sid}:sendMessage`, "POST", { prompt: args.message }); data = { status: "Message sent." }; } else { return error(-32601, "Tool not found"); } // MINIMIZE DATA BEFORE SENDING const optimizedData = minimizeData(name, data); return send({ content: [{ type: "text", text: JSON.stringify(optimizedData, null, 2) }], isError: false }); } catch (err) { return error(-32000, `Jules API Error: ${err.message}`); } } return error(-32601, "Method not found"); } catch (err) { console.error(err); res.status(500).json({ jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal Error" } }); } }; app.post('/mcp', authMiddleware, mcpHandler); app.post('/messages', authMiddleware, mcpHandler); app.get('/', (req, res) => res.send("Jules MCP Optimized")); app.listen(PORT, '0.0.0.0', () => console.log(`Server running on ${PORT}`));