mmty commited on
Commit
a2c5052
·
verified ·
1 Parent(s): 1b3f5fd

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +250 -92
server.js CHANGED
@@ -6,147 +6,305 @@ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch
6
  const app = express();
7
  const PORT = process.env.PORT || 7860;
8
 
9
- // 1. CONFIGURATION
10
- app.set('trust proxy', 1);
11
  app.use(express.json({ limit: '10mb' }));
12
 
13
- // 2. CORS (Global)
14
  app.use(cors({
15
  origin: '*',
16
  methods: ['POST', 'OPTIONS'],
17
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'MCP-Protocol-Version'],
18
  }));
19
 
20
- // 3. LOGGING
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  app.use((req, res, next) => {
22
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
23
  next();
24
  });
25
 
26
- // 4. THE UNIFIED MCP ENDPOINT
27
- app.post('/mcp', async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  try {
29
- const body = req.body;
30
- const { id, method, params } = body;
 
 
 
 
 
 
31
 
32
- console.log(`[POST] Method: ${method} | ID: ${id}`);
33
 
34
- // --- A. INITIALIZE ---
35
  if (method === 'initialize') {
36
- return res.json({
37
- jsonrpc: "2.0",
38
- id: id,
39
- result: {
40
- protocolVersion: "2025-06-18",
41
- capabilities: {
42
- tools: { listChanged: false }
43
- },
44
- serverInfo: { name: "github-mcp-hf", version: "1.0.0" }
45
- }
46
  });
47
  }
48
 
49
- // --- B. NOTIFICATIONS / INITIALIZED (The Fix) ---
50
- // This was causing the 400 error. We must acknowledge it.
51
- if (method === 'notifications/initialized') {
52
- // JSON-RPC notifications don't technically require a response,
53
- // but over HTTP we must return 200 OK to finish the request.
54
- return res.json({
55
- jsonrpc: "2.0",
56
- id: id || null,
57
- result: true
58
- });
59
- }
60
 
61
- // --- C. LIST TOOLS ---
62
  if (method === 'tools/list') {
63
- return res.json({
64
- jsonrpc: "2.0",
65
- id: id,
66
- result: {
67
- tools: [{
68
- name: "getRepoIssues",
69
- description: "Get recent issues from a GitHub repository.",
70
  inputSchema: {
71
  type: "object",
72
  properties: {
73
  owner: { type: "string" },
74
  repo: { type: "string" },
75
- count: { type: "number", default: 5 }
76
  },
77
  required: ["owner", "repo"]
78
  }
79
- }]
80
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  });
82
  }
83
 
84
- // --- D. CALL TOOL ---
85
  if (method === 'tools/call') {
86
  const { name, arguments: args } = params || {};
87
 
88
- if (name === 'getRepoIssues') {
89
- const ghToken = process.env.GH_TOKEN;
90
- if (!ghToken) {
91
- return res.json({
92
- jsonrpc: "2.0", id,
93
- error: { code: -32000, message: "GH_TOKEN missing" }
 
 
 
 
 
94
  });
 
95
  }
96
 
97
- const ghUrl = `https://api.github.com/repos/${args.owner}/${args.repo}/issues?per_page=${args.count || 5}&state=all`;
98
-
99
- const ghRes = await fetch(ghUrl, {
100
- headers: {
101
- 'Authorization': `Bearer ${ghToken}`,
102
- 'User-Agent': 'HuggingFace-MCP',
103
- 'Accept': 'application/vnd.github+json',
104
- 'X-GitHub-Api-Version': '2022-11-28'
105
- }
106
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- if (!ghRes.ok) {
109
- const errText = await ghRes.text();
110
- return res.json({
111
- jsonrpc: "2.0", id,
112
- error: { code: -32000, message: `GitHub API Error: ${ghRes.status}` }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  });
 
 
 
 
 
114
  }
115
 
116
- const data = await ghRes.json();
117
- const content = data.map(i => ({
118
- id: i.number, title: i.title, state: i.state, url: i.html_url
119
- }));
120
-
121
- return res.json({
122
- jsonrpc: "2.0",
123
- id,
124
- result: {
125
- content: [{ type: "text", text: JSON.stringify(content, null, 2) }],
126
- isError: false
127
- }
128
  });
 
 
 
129
  }
130
  }
131
 
132
- // --- E. UNKNOWN METHOD ---
133
- console.warn(`Unknown method: ${method}`);
134
- res.status(400).json({
135
- jsonrpc: "2.0", id,
136
- error: { code: -32601, message: "Method not found" }
137
- });
138
 
139
- } catch (error) {
140
- console.error(error);
141
- res.status(500).json({ error: "Internal Server Error" });
142
  }
143
  });
144
 
145
- // 5. FALLBACK / HEALTH CHECK
146
- app.use((req, res) => {
147
- res.status(200).send("MCP Server Active");
148
- });
149
 
150
- app.listen(PORT, '0.0.0.0', () => {
151
- console.log(`MCP Server running on port ${PORT}`);
152
- });
 
6
  const app = express();
7
  const PORT = process.env.PORT || 7860;
8
 
9
+ // --- 1. CONFIGURATION ---
10
+ app.set('trust proxy', 1); // Required for HF Spaces
11
  app.use(express.json({ limit: '10mb' }));
12
 
13
+ // --- 2. CORS (Global) ---
14
  app.use(cors({
15
  origin: '*',
16
  methods: ['POST', 'OPTIONS'],
17
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'MCP-Protocol-Version'],
18
  }));
19
 
20
+ // --- 3. AUTH MIDDLEWARE ---
21
+ // Secures the MCP server so only you (via Gemini CLI) can use it.
22
+ const authMiddleware = (req, res, next) => {
23
+ const expectedSecret = process.env.MCP_AUTH_TOKEN;
24
+
25
+ // If no secret is set in env, allow open access (Warning logged)
26
+ if (!expectedSecret) {
27
+ // console.warn("[WARN] No MCP_AUTH_TOKEN set. Server is open.");
28
+ return next();
29
+ }
30
+
31
+ const authHeader = req.headers['authorization'];
32
+ if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== expectedSecret) {
33
+ console.error(`[AUTH] Unauthorized access attempt`);
34
+ return res.status(401).json({
35
+ jsonrpc: "2.0",
36
+ id: null,
37
+ error: { code: -32000, message: "Unauthorized: Invalid or missing MCP_AUTH_TOKEN" }
38
+ });
39
+ }
40
+ next();
41
+ };
42
+
43
+ // --- 4. LOGGING ---
44
  app.use((req, res, next) => {
45
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
46
  next();
47
  });
48
 
49
+ // --- 5. GITHUB API HELPER ---
50
+ const callGitHub = async (endpoint, method = 'GET', body = null) => {
51
+ const token = process.env.GH_TOKEN;
52
+ if (!token) throw new Error("Server configuration error: GH_TOKEN is missing.");
53
+
54
+ const url = endpoint.startsWith('http') ? endpoint : `https://api.github.com${endpoint}`;
55
+
56
+ const headers = {
57
+ 'Authorization': `Bearer ${token}`,
58
+ 'Accept': 'application/vnd.github.v3+json',
59
+ 'User-Agent': 'HuggingFace-MCP-Server',
60
+ 'X-GitHub-Api-Version': '2022-11-28'
61
+ };
62
+
63
+ const options = {
64
+ method,
65
+ headers,
66
+ body: body ? JSON.stringify(body) : undefined
67
+ };
68
+
69
+ const response = await fetch(url, options);
70
+
71
+ if (!response.ok) {
72
+ const errorText = await response.text();
73
+ throw new Error(`GitHub API ${response.status}: ${errorText}`);
74
+ }
75
+
76
+ return response.json();
77
+ };
78
+
79
+ // --- 6. MAIN MCP ENDPOINT ---
80
+ app.post('/mcp', authMiddleware, async (req, res) => {
81
  try {
82
+ const { id, method, params } = req.body;
83
+ const requestId = id !== undefined ? id : null;
84
+
85
+ // Helper to send success
86
+ const send = (result) => res.json({ jsonrpc: "2.0", id: requestId, result });
87
+
88
+ // Helper to send error
89
+ const error = (code, msg) => res.json({ jsonrpc: "2.0", id: requestId, error: { code, message: msg } });
90
 
91
+ console.log(`[POST] Method: ${method}`);
92
 
93
+ // --- A. HANDSHAKE & LIFECYCLE ---
94
  if (method === 'initialize') {
95
+ return send({
96
+ protocolVersion: "2024-11-05",
97
+ capabilities: { tools: {}, resources: {}, prompts: {} },
98
+ serverInfo: { name: "github-mcp-versatile", version: "2.0.0" }
 
 
 
 
 
 
99
  });
100
  }
101
 
102
+ if (method === 'notifications/initialized') return send(true);
103
+
104
+ // Crucial for Gemini CLI connection stability
105
+ if (method === 'ping') return send({});
 
 
 
 
 
 
 
106
 
107
+ // --- B. TOOL LIST ---
108
  if (method === 'tools/list') {
109
+ return send({
110
+ tools: [
111
+ // 1. ISSUES
112
+ {
113
+ name: "list_issues",
114
+ description: "List issues in a repo.",
 
115
  inputSchema: {
116
  type: "object",
117
  properties: {
118
  owner: { type: "string" },
119
  repo: { type: "string" },
120
+ state: { type: "string", enum: ["open", "closed", "all"], default: "open" }
121
  },
122
  required: ["owner", "repo"]
123
  }
124
+ },
125
+ {
126
+ name: "create_issue",
127
+ description: "Create a new issue.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ owner: { type: "string" },
132
+ repo: { type: "string" },
133
+ title: { type: "string" },
134
+ body: { type: "string" }
135
+ },
136
+ required: ["owner", "repo", "title"]
137
+ }
138
+ },
139
+ // 2. FILES & CODE
140
+ {
141
+ name: "read_file",
142
+ description: "Read the contents of a specific file.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ owner: { type: "string" },
147
+ repo: { type: "string" },
148
+ path: { type: "string", description: "Path to file (e.g. src/index.js)" },
149
+ branch: { type: "string", description: "Branch or commit SHA (default: main)" }
150
+ },
151
+ required: ["owner", "repo", "path"]
152
+ }
153
+ },
154
+ {
155
+ name: "list_files",
156
+ description: "Get the file tree (recursive) to understand repo structure.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ owner: { type: "string" },
161
+ repo: { type: "string" },
162
+ branch: { type: "string", default: "main" },
163
+ recursive: { type: "boolean", default: true }
164
+ },
165
+ required: ["owner", "repo"]
166
+ }
167
+ },
168
+ {
169
+ name: "search_code",
170
+ description: "Search for code within a repository.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ q: { type: "string", description: "Search query (e.g. 'function login')" },
175
+ owner: { type: "string" },
176
+ repo: { type: "string" }
177
+ },
178
+ required: ["q", "owner", "repo"]
179
+ }
180
+ },
181
+ // 3. PULL REQUESTS
182
+ {
183
+ name: "list_prs",
184
+ description: "List Pull Requests.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {
188
+ owner: { type: "string" },
189
+ repo: { type: "string" },
190
+ state: { type: "string", enum: ["open", "closed", "all"], default: "open" }
191
+ },
192
+ required: ["owner", "repo"]
193
+ }
194
+ },
195
+ {
196
+ name: "create_pr",
197
+ description: "Create a Pull Request.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ owner: { type: "string" },
202
+ repo: { type: "string" },
203
+ title: { type: "string" },
204
+ head: { type: "string", description: "The name of the branch where your changes are implemented." },
205
+ base: { type: "string", description: "The name of the branch you want the changes pulled into." },
206
+ body: { type: "string" }
207
+ },
208
+ required: ["owner", "repo", "title", "head", "base"]
209
+ }
210
+ }
211
+ ]
212
  });
213
  }
214
 
215
+ // --- C. TOOL EXECUTION ---
216
  if (method === 'tools/call') {
217
  const { name, arguments: args } = params || {};
218
 
219
+ try {
220
+ let resultData;
221
+
222
+ // --- Logic: ISSUES ---
223
+ if (name === "list_issues") {
224
+ const data = await callGitHub(`/repos/${args.owner}/${args.repo}/issues?state=${args.state || 'open'}&per_page=10`);
225
+ resultData = data.map(i => ({ number: i.number, title: i.title, state: i.state, url: i.html_url }));
226
+ }
227
+ else if (name === "create_issue") {
228
+ const data = await callGitHub(`/repos/${args.owner}/${args.repo}/issues`, 'POST', {
229
+ title: args.title, body: args.body
230
  });
231
+ resultData = { message: "Issue Created", url: data.html_url, number: data.number };
232
  }
233
 
234
+ // --- Logic: FILES ---
235
+ else if (name === "read_file") {
236
+ // GitHub API returns content in Base64
237
+ const ref = args.branch ? `?ref=${args.branch}` : '';
238
+ const data = await callGitHub(`/repos/${args.owner}/${args.repo}/contents/${args.path}${ref}`);
239
+
240
+ if (Array.isArray(data)) return error(-32000, "Path is a directory, not a file.");
241
+ if (!data.content) return error(-32000, "File content empty or binary.");
242
+
243
+ const decodedContent = Buffer.from(data.content, 'base64').toString('utf-8');
244
+ // Truncate if huge to save tokens
245
+ const safeContent = decodedContent.length > 20000 ? decodedContent.substring(0, 20000) + "\n...[Truncated]" : decodedContent;
246
+ resultData = safeContent;
247
+ }
248
+ else if (name === "list_files") {
249
+ const ref = args.branch || 'main';
250
+ // Get the tree SHA first, or just assume main/master
251
+ // To be simple, we use the trees API which requires the SHA of the branch.
252
+ // Fallback: Use the 'contents' API recursively? No, that's heavy.
253
+ // Better approach for LLM: Get the HEAD commit SHA first.
254
+
255
+ // 1. Get Branch SHA
256
+ const branchData = await callGitHub(`/repos/${args.owner}/${args.repo}/git/ref/heads/${ref}`);
257
+ const sha = branchData.object.sha;
258
 
259
+ // 2. Get Tree
260
+ const recursive = args.recursive ? '?recursive=1' : '';
261
+ const treeData = await callGitHub(`/repos/${args.owner}/${args.repo}/git/trees/${sha}${recursive}`);
262
+
263
+ // Filter to just paths to save tokens
264
+ resultData = treeData.tree.map(t => t.path).slice(0, 200); // Limit to 200 files
265
+ }
266
+ else if (name === "search_code") {
267
+ const q = `${args.q} repo:${args.owner}/${args.repo}`;
268
+ const data = await callGitHub(`/search/code?q=${encodeURIComponent(q)}&per_page=5`);
269
+ resultData = data.items.map(i => ({ name: i.name, path: i.path, url: i.html_url }));
270
+ }
271
+
272
+ // --- Logic: PULL REQUESTS ---
273
+ else if (name === "list_prs") {
274
+ const data = await callGitHub(`/repos/${args.owner}/${args.repo}/pulls?state=${args.state || 'open'}&per_page=10`);
275
+ resultData = data.map(i => ({ number: i.number, title: i.title, state: i.state, url: i.html_url }));
276
+ }
277
+ else if (name === "create_pr") {
278
+ const data = await callGitHub(`/repos/${args.owner}/${args.repo}/pulls`, 'POST', {
279
+ title: args.title, head: args.head, base: args.base, body: args.body
280
  });
281
+ resultData = { message: "PR Created", url: data.html_url, number: data.number };
282
+ }
283
+
284
+ else {
285
+ return error(-32601, `Tool ${name} not found`);
286
  }
287
 
288
+ // Return Standard Success Response
289
+ return send({
290
+ content: [{ type: "text", text: typeof resultData === 'string' ? resultData : JSON.stringify(resultData, null, 2) }],
291
+ isError: false
 
 
 
 
 
 
 
 
292
  });
293
+
294
+ } catch (err) {
295
+ return error(-32000, `Execution Error: ${err.message}`);
296
  }
297
  }
298
 
299
+ return error(-32601, "Method not found");
 
 
 
 
 
300
 
301
+ } catch (err) {
302
+ console.error(err);
303
+ res.status(500).json({ jsonrpc: "2.0", id: null, error: { code: -32603, message: "Internal Server Error" } });
304
  }
305
  });
306
 
307
+ // 7. HEALTH CHECK
308
+ app.use((req, res) => res.status(200).send("GitHub MCP Active"));
 
 
309
 
310
+ app.listen(PORT, '0.0.0.0', () => console.log(`Server running on ${PORT}`));