File size: 9,051 Bytes
58e38db 9109b5e 58e38db 51ef067 58e38db 51ef067 58e38db 51ef067 9109b5e 51ef067 58e38db 51ef067 58e38db 51ef067 58e38db 51ef067 58e38db 9109b5e 51ef067 9109b5e 51ef067 9109b5e 51ef067 9109b5e 51ef067 9109b5e 51ef067 9109b5e 51ef067 9109b5e 51ef067 58e38db 51ef067 58e38db 51ef067 9109b5e 51ef067 58e38db 51ef067 58e38db 51ef067 58e38db 51ef067 58e38db 51ef067 58e38db 51ef067 58e38db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { SSEServerTransport } = require("@modelcontextprotocol/sdk/server/sse.js");
const {
ListToolsRequestSchema,
CallToolRequestSchema
} = require("@modelcontextprotocol/sdk/types.js");
const express = require("express");
const axios = require("axios");
const app = express();
const API_BASE = "https://jules.googleapis.com/v1alpha";
app.use(express.json());
/**
* We use a Map to keep track of multiple active transports.
* This allows multiple users/clients to connect simultaneously without crashing.
*/
const transports = new Map();
/**
* Helper for Google API Requests using the user-provided API Key
*/
const julesRequest = async (method, endpoint, apiKey, data = null) => {
if (!apiKey) {
throw new Error("Missing Jules API Key. Please provide 'apiKey' in the tool arguments.");
}
try {
const response = await axios({
method,
url: `${API_BASE}/${endpoint}`,
headers: {
"X-Goog-Api-Key": apiKey,
"Content-Type": "application/json",
},
data,
});
return response.data;
} catch (error) {
throw new Error(error.response?.data?.error?.message || error.message);
}
};
/**
* Creates a fresh server instance with all tool definitions.
* This prevents the "Already connected to a transport" error.
*/
function createJulesServer() {
const server = new Server(
{
name: "jules-mcp-user-auth",
version: "1.2.0",
},
{
capabilities: {
tools: {},
},
}
);
// 1. Define the List Tools Handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_sources",
description: "List available GitHub repositories connected to Jules.",
inputSchema: {
type: "object",
properties: {
apiKey: { type: "string", description: "Your Jules API Key" }
},
required: ["apiKey"]
},
},
{
name: "create_session",
description: "Start a new coding task with Jules.",
inputSchema: {
type: "object",
properties: {
apiKey: { type: "string", description: "Your Jules API Key" },
prompt: { type: "string", description: "What do you want Jules to do?" },
source: { type: "string", description: "Source name (e.g., sources/github/user/repo)" },
title: { type: "string", description: "Title for the session" },
branch: { type: "string", description: "Starting branch (default: main)", default: "main" },
autoPR: { type: "boolean", description: "Automatically create a PR?", default: false }
},
required: ["apiKey", "prompt", "source", "title"],
},
},
{
name: "approve_plan",
description: "Approve a plan generated by Jules for a specific session.",
inputSchema: {
type: "object",
properties: {
apiKey: { type: "string", description: "Your Jules API Key" },
sessionId: { type: "string", description: "The ID of the session" }
},
required: ["apiKey", "sessionId"],
},
},
{
name: "get_activities",
description: "List activities and progress for a session.",
inputSchema: {
type: "object",
properties: {
apiKey: { type: "string", description: "Your Jules API Key" },
sessionId: { type: "string", description: "The ID of the session" }
},
required: ["apiKey", "sessionId"],
},
},
{
name: "send_message",
description: "Send a follow-up message to Jules during a session.",
inputSchema: {
type: "object",
properties: {
apiKey: { type: "string", description: "Your Jules API Key" },
sessionId: { type: "string", description: "The ID of the session" },
prompt: { type: "string", description: "Your message to the agent" }
},
required: ["apiKey", "sessionId", "prompt"],
},
}
],
};
});
// 2. Define the Call Tool Handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const userApiKey = args.apiKey;
try {
switch (name) {
case "list_sources":
const sources = await julesRequest("GET", "sources", userApiKey);
return { content: [{ type: "text", text: JSON.stringify(sources, null, 2) }] };
case "create_session":
const sessionData = {
prompt: args.prompt,
title: args.title,
sourceContext: {
source: args.source,
githubRepoContext: { startingBranch: args.branch || "main" }
},
automationMode: args.autoPR ? "AUTO_CREATE_PR" : undefined
};
const newSession = await julesRequest("POST", "sessions", userApiKey, sessionData);
return { content: [{ type: "text", text: JSON.stringify(newSession, null, 2) }] };
case "approve_plan":
const approval = await julesRequest("POST", `sessions/${args.sessionId}:approvePlan`, userApiKey);
return { content: [{ type: "text", text: JSON.stringify(approval, null, 2) }] };
case "get_activities":
const activities = await julesRequest("GET", `sessions/${args.sessionId}/activities?pageSize=50`, userApiKey);
return { content: [{ type: "text", text: JSON.stringify(activities, null, 2) }] };
case "send_message":
const message = await julesRequest("POST", `sessions/${args.sessionId}:sendMessage`, userApiKey, { prompt: args.prompt });
return { content: [{ type: "text", text: JSON.stringify(message, null, 2) }] };
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (err) {
return { isError: true, content: [{ type: "text", text: err.message }] };
}
});
return server;
}
app.get("/sse", async (req, res) => {
console.log("New SSE connection attempt...");
// Create a new server and transport for EVERY connection
const server = createJulesServer();
const transport = new SSEServerTransport("/messages", res);
// Store the transport so the POST route can find it by sessionId
transports.set(transport.sessionId, transport);
// Cleanup when connection closes
res.on("close", () => {
console.log(`Closing transport for session: ${transport.sessionId}`);
transports.delete(transport.sessionId);
});
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId;
const transport = transports.get(sessionId);
if (!transport) {
return res.status(404).send("Session not found");
}
await transport.handlePostMessage(req, res);
});
app.get("/", (req, res) => {
const host = req.get('host');
const protocol = req.get('x-forwarded-proto') || req.protocol;
const fullUrl = `${protocol}://${host}/sse`;
res.send(`
<html>
<head>
<title>Jules Dynamic MCP Server</title>
<style>
body { font-family: -apple-system, sans-serif; line-height: 1.6; padding: 40px; color: #333; max-width: 800px; margin: auto; background: #f9f9f9; }
.card { background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
code { background: #f0f0f0; padding: 4px 8px; border-radius: 4px; font-weight: bold; color: #d63384; }
pre { background: #1e1e1e; color: #569cd6; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #333; }
.url { color: #4caf50; font-weight: bold; }
.step { margin-bottom: 20px; padding-left: 20px; border-left: 4px solid #007bff; }
</style>
</head>
<body>
<div class="card">
<h1>🚀 Jules API MCP Server</h1>
<p>Your dynamic MCP server for Google Jules is ready!</p>
<div class="step">
<h3>1. Copy SSE URL</h3>
<p>Add this URL to your MCP client (Claude Desktop or Cursor):</p>
<pre class="url">${fullUrl}</pre>
</div>
<div class="step">
<h3>2. Provide API Key</h3>
<p>Every tool call requires your <code>apiKey</code>. You can obtain it from the <strong>Jules Web App Settings</strong>.</p>
</div>
<div class="step">
<h3>3. Usage Tip</h3>
<p>Tell the AI: <i>"Using the Jules MCP, list my sources. Here is my API key: [YOUR_KEY]"</i></p>
</div>
</div>
</body>
</html>
`);
});
const PORT = process.env.PORT || 7860;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
}); |