import { createMCPServer } from '../src'; import { InMemoryStore } from '../src/stores'; import { Octokit } from '@octokit/rest'; import ngrok from '@ngrok/ngrok'; import crypto from 'crypto'; /** * GitHub MCP Server - LIVE Multi-User Example * * This example supports multiple users with different GitHub credentials. * Each user is authenticated via a token, and their credentials are fetched * from a credentials store (simulated here, but would be a service in production). * * Prerequisites: * 1. ngrok auth token for public URL tunneling * 2. User tokens configured in CREDENTIALS_STORE below */ /** * User credentials interface */ interface UserCredentials { githubToken: string; githubOwner: string; githubRepo: string; githubLogin: string; } /** * Simulated credentials store * In production, this would be replaced by a service call * that fetches credentials based on token and MCP server name */ const CREDENTIALS_STORE = new Map([ // Example user 1 [ 'user-token-123', { githubToken: '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', githubOwner: 'surajbhan', githubRepo: 'test', githubLogin: 'surajbhan', }, ], // Example user 2 [ 'user-token-456', { githubToken: 'ghp_example_token_2', githubOwner: 'anotheruser', githubRepo: 'demo', githubLogin: 'anotheruser', }, ], // Add more users as needed ]); /** * Fetch user credentials from the store (simulates a service call) * In production, this would make an HTTP call to a credentials service */ async function fetchCredentials( token: string, mcpServerName: string ): Promise { // Simulate async service call await new Promise((resolve) => setTimeout(resolve, 10)); // In production, this would be: // const response = await fetch(`https://credentials-service.com/api/credentials`, { // method: 'POST', // headers: { 'Authorization': `Bearer ${token}` }, // body: JSON.stringify({ mcpServerName }) // }); // return await response.json(); return CREDENTIALS_STORE.get(token) || null; } /** * Create an Octokit instance for a specific user */ function createOctokit(credentials: UserCredentials): Octokit { return new Octokit({ auth: credentials.githubToken }); } async function main() { console.log('╔═══════════════════════════════════════════════════════════╗'); console.log('║ GitHub MCP Server - Multi-User Live Integration ║'); console.log('╚═══════════════════════════════════════════════════════════╝'); console.log(''); // Configuration const ngrokAuthToken = process.env.NGROK_AUTH_TOKEN || '343iE4JabU5sRNM235xJ0N1iFQK_4GHuxi3va6GFVDn5pW92W'; const webhookSecret = process.env.WEBHOOK_SECRET || crypto.randomBytes(32).toString('hex'); const mcpServerName = 'github-mcp-live'; console.log('🚀 Starting multi-user MCP server...'); console.log(''); // Start ngrok tunnel console.log('🌐 Starting ngrok tunnel...'); let ngrokUrl: string; try { ngrokUrl = await ngrok .connect({ addr: 3000, authtoken: ngrokAuthToken || undefined, }) .then((x) => { const url = x.url(); console.log(`✅ ngrok tunnel established: ${url}`); if (!url) { console.log('❌ ngrok did not return a URL!'); process.exit(1); } return url; }) .catch((err) => { console.log('❌ Failed to start ngrok tunnel:', err.message); console.log(' Set NGROK_AUTH_TOKEN environment variable'); process.exit(1); }); console.log(`✅ Public URL: ${ngrokUrl}`); console.log(''); } catch (error: any) { console.log('❌ Failed to start ngrok tunnel:', error.message); console.log(''); console.log(' Get your ngrok auth token:'); console.log(' https://dashboard.ngrok.com/get-started/your-authtoken'); console.log(' Then set: export NGROK_AUTH_TOKEN="your_token"'); process.exit(1); } // Store for webhook IDs (keyed by subscriptionId) const webhookStore = new Map< string, { hookId: number; userId: string; owner: string; repo: string } >(); const store = new InMemoryStore(); const server = createMCPServer({ name: 'github-mcp-live', version: '1.0.0', publicUrl: ngrokUrl, port: 3000, store, tools: [ { name: 'create_issue', description: 'Create a GitHub issue', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Issue title' }, body: { type: 'string', description: 'Issue body' }, labels: { type: 'array', items: { type: 'string' }, description: 'Issue labels', }, }, required: ['title'], }, handler: async (input, context) => { console.log('📝 Creating GitHub issue:', input.title); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return { success: false, error: 'User not authenticated or credentials not found', }; } const octokit = createOctokit(credentials); try { const { data: issue } = await octokit.issues.create({ owner: credentials.githubOwner, repo: credentials.githubRepo, title: input.title, body: input.body, labels: input.labels, }); console.log(`✅ Issue created: #${issue.number} (user: ${credentials.githubLogin})`); return { success: true, issue: { number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, created_at: issue.created_at, }, }; } catch (error: any) { console.log('❌ Failed to create issue:', error.message); return { success: false, error: error.message, }; } }, }, { name: 'list_issues', description: 'List issues from the repository', inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'Filter by state: open, closed, all', enum: ['open', 'closed', 'all'], }, limit: { type: 'number', description: 'Number of issues to return', }, }, }, handler: async (input, context) => { console.log('📋 Listing GitHub issues...'); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return { success: false, error: 'User not authenticated or credentials not found', }; } const octokit = createOctokit(credentials); try { const { data: issues } = await octokit.issues.listForRepo({ owner: credentials.githubOwner, repo: credentials.githubRepo, state: (input.state as any) || 'open', per_page: input.limit || 10, }); console.log(`✅ Listed ${issues.length} issues (user: ${credentials.githubLogin})`); return { success: true, count: issues.length, issues: issues.map((issue: any) => ({ number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, created_at: issue.created_at, labels: issue.labels.map((l: any) => l.name), })), }; } catch (error: any) { console.log('❌ Failed to list issues:', error.message); return { success: false, error: error.message, }; } }, }, ], resources: [ { uri: 'github://repo/{owner}/{repo}/issues', name: 'GitHub Repository Issues', description: 'Live issues from GitHub repository', mimeType: 'application/json', read: async (uri, context) => { console.log('📖 Reading repository issues...'); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { throw new Error('User not authenticated or credentials not found'); } const octokit = createOctokit(credentials); try { const { data: issues } = await octokit.issues.listForRepo({ owner: credentials.githubOwner, repo: credentials.githubRepo, state: 'open', per_page: 100, }); console.log(`✅ Read ${issues.length} issues (user: ${credentials.githubLogin})`); return { contents: issues.map((issue: any) => ({ number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, labels: issue.labels.map((l: any) => l.name), })), }; } catch (error: any) { console.log('❌ Failed to read issues:', error.message); throw error; } }, list: async (context) => { // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return []; } return [ { uri: `github://repo/${credentials.githubOwner}/${credentials.githubRepo}/issues`, name: `${credentials.githubOwner}/${credentials.githubRepo} Issues`, description: `Live issues from ${credentials.githubOwner}/${credentials.githubRepo} repository`, }, ]; }, subscription: { onSubscribe: async (uri, subscriptionId, thirdPartyWebhookUrl, context) => { console.log(''); console.log('🔔 Setting up GitHub webhook...'); console.log(` Resource: ${uri}`); console.log(` Subscription ID: ${subscriptionId}`); console.log(` Webhook URL: ${thirdPartyWebhookUrl}`); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { throw new Error('User not authenticated or credentials not found'); } const octokit = createOctokit(credentials); try { // Create webhook on GitHub const { data: hook } = await octokit.repos.createWebhook({ owner: credentials.githubOwner, repo: credentials.githubRepo, config: { url: thirdPartyWebhookUrl, content_type: 'json', secret: webhookSecret, insecure_ssl: '0', }, events: ['issues', 'issue_comment'], active: true, }); webhookStore.set(subscriptionId, { hookId: hook.id, userId: context.userId, owner: credentials.githubOwner, repo: credentials.githubRepo, }); console.log( `✅ GitHub webhook created: ID ${hook.id} (user: ${credentials.githubLogin})` ); console.log(''); return { thirdPartyWebhookId: hook.id.toString(), metadata: { owner: credentials.githubOwner, repo: credentials.githubRepo, events: ['issues', 'issue_comment'], webhook_url: hook.config.url, user: credentials.githubLogin, }, }; } catch (error: any) { console.log('❌ Failed to create webhook:', error.message); throw error; } }, onUnsubscribe: async (uri, subscriptionId, storedData, context) => { console.log(''); console.log('🗑️ Removing GitHub webhook...'); console.log(` Subscription ID: ${subscriptionId}`); const webhookInfo = webhookStore.get(subscriptionId); if (!webhookInfo) { console.log('⚠️ Webhook ID not found in store'); return; } // Get user credentials from context (or use stored info) const credentials = context.credentials as UserCredentials; if (!credentials) { console.log('⚠️ User credentials not available for cleanup'); return; } const octokit = createOctokit(credentials); try { await octokit.repos.deleteWebhook({ owner: webhookInfo.owner, repo: webhookInfo.repo, hook_id: webhookInfo.hookId, }); webhookStore.delete(subscriptionId); console.log(`✅ GitHub webhook deleted: ID ${webhookInfo.hookId}`); console.log(''); } catch (error: any) { console.log('❌ Failed to delete webhook:', error.message); } }, onWebhook: async (subscriptionId, payload, headers) => { const event = headers['x-github-event']; console.log(''); console.log('📬 Received GitHub webhook'); console.log(` Event: ${event}`); console.log(` Subscription: ${subscriptionId}`); if (event === 'issues') { const { action, issue, repository } = payload; console.log(` Action: ${action}`); console.log(` Issue: #${issue.number} - ${issue.title}`); if (['opened', 'edited', 'closed', 'reopened'].includes(action)) { return { resourceUri: `github://repo/${repository.owner.login}/${repository.name}/issues/${issue.number}`, changeType: action === 'opened' ? 'created' : action === 'closed' ? 'deleted' : 'updated', data: { issueNumber: issue.number, title: issue.title, state: issue.state, action, html_url: issue.html_url, }, }; } } if (event === 'issue_comment') { const { action, issue, comment, repository } = payload; console.log(` Comment ${action} on issue #${issue.number}`); return { resourceUri: `github://repo/${repository.owner.login}/${repository.name}/issues`, changeType: 'updated', data: { issueNumber: issue.number, commentAction: action, comment: { id: comment.id, body: comment.body, user: comment.user.login, }, }, }; } console.log(' ℹ️ Event not mapped to resource change'); return null; }, }, }, { uri: 'github://repo/{owner}/{repo}/issues/{issue_number}', name: 'GitHub Repository Issue Detail', description: 'Live issue details from GitHub repository', mimeType: 'application/json', read: async (uri, context) => { console.log('📖 Reading repository issue detail...'); // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { throw new Error('User not authenticated or credentials not found'); } const parts = uri.split('/'); const owner = parts[3]; const repo = parts[4]; const issue_number = parseInt(parts[6], 10); const octokit = createOctokit(credentials); try { const { data: issue } = await octokit.issues.get({ owner, repo, issue_number, }); return { contents: [ { number: issue.number, title: issue.title, state: issue.state, html_url: issue.html_url, created_at: issue.created_at, updated_at: issue.updated_at, labels: issue.labels.map((l: any) => l.name), }, ], }; } catch (error: any) { console.log('❌ Failed to read issue detail:', error.message); throw error; } }, list: async (context) => { // Get user credentials from context const credentials = context.credentials as UserCredentials; if (!credentials) { return []; } return [ { uri: `github://repo/${credentials.githubOwner}/${credentials.githubRepo}/issues/{issue_number}`, name: `${credentials.githubOwner}/${credentials.githubRepo} Issues`, description: `Live issues from ${credentials.githubOwner}/${credentials.githubRepo} repository`, }, ]; } }, ], webhooks: { incomingPath: '/webhooks/incoming', incomingSecret: webhookSecret, verifyIncomingSignature: (payload, signature, secret) => { if (!signature) return false; const hmac = crypto.createHmac('sha256', secret); hmac.update(JSON.stringify(payload)); const expected = `sha256=${hmac.digest('hex')}`; try { return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } catch { return false; } }, outgoing: { timeout: 5000, retries: 3, retryDelay: 1000, signPayload: (payload, secret) => { const hmac = crypto.createHmac('sha256', secret); hmac.update(JSON.stringify(payload)); return `sha256=${hmac.digest('hex')}`; }, }, }, // Authentication: Extract token and fetch credentials authenticate: async (req) => { // Extract token from Authorization header const authHeader = req.headers.authorization; console.log('Authorization Header:', authHeader); if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new Error('Missing or invalid Authorization header'); } const token = authHeader.substring(7); // Remove 'Bearer ' prefix // Fetch credentials from store (or service in production) const credentials = await fetchCredentials(token, mcpServerName); if (!credentials) { throw new Error('Invalid token or credentials not found'); } console.log(`✅ User authenticated: ${credentials.githubLogin}`); return { userId: credentials.githubLogin, credentials, // Store credentials in context for later use }; }, logLevel: 'info', }); await server.start(); console.log(''); console.log('╔═══════════════════════════════════════════════════════════╗'); console.log('║ 🎉 GitHub MCP Server is LIVE! (Multi-User) ║'); console.log('╚═══════════════════════════════════════════════════════════╝'); console.log(''); console.log('📍 MCP Endpoint:'); console.log(` ${ngrokUrl}/mcp`); console.log(''); console.log('� Authentication:'); console.log(' Add header: Authorization: Bearer '); console.log(' Example tokens: user-token-123, user-token-456'); console.log(''); console.log('�🔍 Test with MCP Inspector:'); console.log(` npx @modelcontextprotocol/inspector ${ngrokUrl}/mcp`); console.log(''); console.log('📋 Available Tools:'); console.log(' - create_issue: Create a new GitHub issue'); console.log(' - list_issues: List repository issues'); console.log(''); console.log('📚 Available Resources (dynamic per user):'); console.log(' - github://repo/{owner}/{repo}/issues'); console.log(''); console.log('🔔 Webhook Subscription:'); console.log(' 1. Authenticate with user token'); console.log(' 2. Use MCP Inspector or POST to /mcp with resources/subscribe'); console.log(' 3. Provide your callback URL in _meta.webhookUrl'); console.log(" 4. GitHub webhook will be created for user's repo"); console.log(' 5. Create/edit/close issues to see live updates!'); console.log(''); console.log('📝 Example: Create a test issue'); console.log(' curl -X POST http://localhost:3000/mcp \\'); console.log(' -H "Content-Type: application/json" \\'); console.log(' -H "Authorization: Bearer user-token-123" \\'); console.log(' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/call",'); console.log(' "params":{"name":"create_issue",'); console.log(' "arguments":{"title":"Test Issue"}}}\''); console.log(''); console.log('⚠️ Press Ctrl+C to stop and cleanup webhooks'); console.log(''); // Graceful shutdown const cleanup = async () => { console.log(''); console.log('🧹 Shutting down...'); // Delete all webhooks for (const [subscriptionId, webhookInfo] of webhookStore.entries()) { try { // Try to get credentials for cleanup const credentials = CREDENTIALS_STORE.get(`user-token-${webhookInfo.userId}`); if (credentials) { const octokit = createOctokit(credentials); await octokit.repos.deleteWebhook({ owner: webhookInfo.owner, repo: webhookInfo.repo, hook_id: webhookInfo.hookId, }); console.log(`✅ Deleted webhook: ${webhookInfo.hookId} (user: ${webhookInfo.userId})`); } else { console.log(`⚠️ Cannot delete webhook ${webhookInfo.hookId}: credentials not found`); } } catch (error: any) { console.log(`⚠️ Failed to delete webhook ${webhookInfo.hookId}: ${error.message}`); } } await server.stop(); await ngrok.disconnect(); await ngrok.kill(); store.destroy(); console.log('👋 Goodbye!'); process.exit(0); }; process.on('SIGTERM', cleanup); process.on('SIGINT', cleanup); } main().catch((error) => { console.error('💥 Fatal error:', error); process.exit(1); });