MCP / server.js
mmty's picture
Update server.js
a2c5052 verified
Raw History Blame Contribute Delete
11.3 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 = process.env.PORT || 7860;
// --- 1. CONFIGURATION ---
app.set('trust proxy', 1); // Required for HF Spaces
app.use(express.json({ limit: '10mb' }));
// --- 2. CORS (Global) ---
app.use(cors({
origin: '*',
methods: ['POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'MCP-Protocol-Version'],
}));
// --- 3. AUTH MIDDLEWARE ---
// Secures the MCP server so only you (via Gemini CLI) can use it.
const authMiddleware = (req, res, next) => {
const expectedSecret = process.env.MCP_AUTH_TOKEN;
// If no secret is set in env, allow open access (Warning logged)
if (!expectedSecret) {
// console.warn("[WARN] No MCP_AUTH_TOKEN set. Server is open.");
return next();
}
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.split(' ')[1] !== expectedSecret) {
console.error(`[AUTH] Unauthorized access attempt`);
return res.status(401).json({
jsonrpc: "2.0",
id: null,
error: { code: -32000, message: "Unauthorized: Invalid or missing MCP_AUTH_TOKEN" }
});
}
next();
};
// --- 4. LOGGING ---
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// --- 5. GITHUB API HELPER ---
const callGitHub = async (endpoint, method = 'GET', body = null) => {
const token = process.env.GH_TOKEN;
if (!token) throw new Error("Server configuration error: GH_TOKEN is missing.");
const url = endpoint.startsWith('http') ? endpoint : `https://api.github.com${endpoint}`;
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'HuggingFace-MCP-Server',
'X-GitHub-Api-Version': '2022-11-28'
};
const options = {
method,
headers,
body: body ? JSON.stringify(body) : undefined
};
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`GitHub API ${response.status}: ${errorText}`);
}
return response.json();
};
// --- 6. MAIN MCP ENDPOINT ---
app.post('/mcp', authMiddleware, async (req, res) => {
try {
const { id, method, params } = req.body;
const requestId = id !== undefined ? id : null;
// Helper to send success
const send = (result) => res.json({ jsonrpc: "2.0", id: requestId, result });
// Helper to send error
const error = (code, msg) => res.json({ jsonrpc: "2.0", id: requestId, error: { code, message: msg } });
console.log(`[POST] Method: ${method}`);
// --- A. HANDSHAKE & LIFECYCLE ---
if (method === 'initialize') {
return send({
protocolVersion: "2024-11-05",
capabilities: { tools: {}, resources: {}, prompts: {} },
serverInfo: { name: "github-mcp-versatile", version: "2.0.0" }
});
}
if (method === 'notifications/initialized') return send(true);
// Crucial for Gemini CLI connection stability
if (method === 'ping') return send({});
// --- B. TOOL LIST ---
if (method === 'tools/list') {
return send({
tools: [
// 1. ISSUES
{
name: "list_issues",
description: "List issues in a repo.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
state: { type: "string", enum: ["open", "closed", "all"], default: "open" }
},
required: ["owner", "repo"]
}
},
{
name: "create_issue",
description: "Create a new issue.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
title: { type: "string" },
body: { type: "string" }
},
required: ["owner", "repo", "title"]
}
},
// 2. FILES & CODE
{
name: "read_file",
description: "Read the contents of a specific file.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
path: { type: "string", description: "Path to file (e.g. src/index.js)" },
branch: { type: "string", description: "Branch or commit SHA (default: main)" }
},
required: ["owner", "repo", "path"]
}
},
{
name: "list_files",
description: "Get the file tree (recursive) to understand repo structure.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
branch: { type: "string", default: "main" },
recursive: { type: "boolean", default: true }
},
required: ["owner", "repo"]
}
},
{
name: "search_code",
description: "Search for code within a repository.",
inputSchema: {
type: "object",
properties: {
q: { type: "string", description: "Search query (e.g. 'function login')" },
owner: { type: "string" },
repo: { type: "string" }
},
required: ["q", "owner", "repo"]
}
},
// 3. PULL REQUESTS
{
name: "list_prs",
description: "List Pull Requests.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
state: { type: "string", enum: ["open", "closed", "all"], default: "open" }
},
required: ["owner", "repo"]
}
},
{
name: "create_pr",
description: "Create a Pull Request.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
title: { type: "string" },
head: { type: "string", description: "The name of the branch where your changes are implemented." },
base: { type: "string", description: "The name of the branch you want the changes pulled into." },
body: { type: "string" }
},
required: ["owner", "repo", "title", "head", "base"]
}
}
]
});
}
// --- C. TOOL EXECUTION ---
if (method === 'tools/call') {
const { name, arguments: args } = params || {};
try {
let resultData;
// --- Logic: ISSUES ---
if (name === "list_issues") {
const data = await callGitHub(`/repos/${args.owner}/${args.repo}/issues?state=${args.state || 'open'}&per_page=10`);
resultData = data.map(i => ({ number: i.number, title: i.title, state: i.state, url: i.html_url }));
}
else if (name === "create_issue") {
const data = await callGitHub(`/repos/${args.owner}/${args.repo}/issues`, 'POST', {
title: args.title, body: args.body
});
resultData = { message: "Issue Created", url: data.html_url, number: data.number };
}
// --- Logic: FILES ---
else if (name === "read_file") {
// GitHub API returns content in Base64
const ref = args.branch ? `?ref=${args.branch}` : '';
const data = await callGitHub(`/repos/${args.owner}/${args.repo}/contents/${args.path}${ref}`);
if (Array.isArray(data)) return error(-32000, "Path is a directory, not a file.");
if (!data.content) return error(-32000, "File content empty or binary.");
const decodedContent = Buffer.from(data.content, 'base64').toString('utf-8');
// Truncate if huge to save tokens
const safeContent = decodedContent.length > 20000 ? decodedContent.substring(0, 20000) + "\n...[Truncated]" : decodedContent;
resultData = safeContent;
}
else if (name === "list_files") {
const ref = args.branch || 'main';
// Get the tree SHA first, or just assume main/master
// To be simple, we use the trees API which requires the SHA of the branch.
// Fallback: Use the 'contents' API recursively? No, that's heavy.
// Better approach for LLM: Get the HEAD commit SHA first.
// 1. Get Branch SHA
const branchData = await callGitHub(`/repos/${args.owner}/${args.repo}/git/ref/heads/${ref}`);
const sha = branchData.object.sha;
// 2. Get Tree
const recursive = args.recursive ? '?recursive=1' : '';
const treeData = await callGitHub(`/repos/${args.owner}/${args.repo}/git/trees/${sha}${recursive}`);
// Filter to just paths to save tokens
resultData = treeData.tree.map(t => t.path).slice(0, 200); // Limit to 200 files
}
else if (name === "search_code") {
const q = `${args.q} repo:${args.owner}/${args.repo}`;
const data = await callGitHub(`/search/code?q=${encodeURIComponent(q)}&per_page=5`);
resultData = data.items.map(i => ({ name: i.name, path: i.path, url: i.html_url }));
}
// --- Logic: PULL REQUESTS ---
else if (name === "list_prs") {
const data = await callGitHub(`/repos/${args.owner}/${args.repo}/pulls?state=${args.state || 'open'}&per_page=10`);
resultData = data.map(i => ({ number: i.number, title: i.title, state: i.state, url: i.html_url }));
}
else if (name === "create_pr") {
const data = await callGitHub(`/repos/${args.owner}/${args.repo}/pulls`, 'POST', {
title: args.title, head: args.head, base: args.base, body: args.body
});
resultData = { message: "PR Created", url: data.html_url, number: data.number };
}
else {
return error(-32601, `Tool ${name} not found`);
}
// Return Standard Success Response
return send({
content: [{ type: "text", text: typeof resultData === 'string' ? resultData : JSON.stringify(resultData, null, 2) }],
isError: false
});
} catch (err) {
return error(-32000, `Execution 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 Server Error" } });
}
});
// 7. HEALTH CHECK
app.use((req, res) => res.status(200).send("GitHub MCP Active"));
app.listen(PORT, '0.0.0.0', () => console.log(`Server running on ${PORT}`));