julesmcp / server.js
mmty's picture
Update server.js
47b8df3 verified
Raw History Blame Contribute Delete
8.52 kB
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}`));