UNPKG

onerios-mcp-server

Version:

OneriosMCP server providing memory, backlog management, file operations, and utility functions for enhanced AI assistant capabilities

346 lines (345 loc) 12.2 kB
"use strict"; /** * GitHub API Integration Module * * Provides secure authentication and API interaction capabilities for GitHub Projects v2 API. * Supports Personal Access Tokens with proper validation and error handling. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.validateGitHubToken = exports.checkGitHubAuth = exports.authenticateGitHub = exports.GitHubTokenValidator = exports.GitHubAPIClient = void 0; exports.getGitHubClient = getGitHubClient; exports.setGitHubClient = setGitHubClient; const zod_1 = require("zod"); // GitHub API Configuration Schema const GitHubConfigSchema = zod_1.z.object({ token: zod_1.z.string().min(1, 'GitHub token is required'), baseUrl: zod_1.z.string().url().optional().default('https://api.github.com'), graphqlUrl: zod_1.z.string().url().optional().default('https://api.github.com/graphql'), timeout: zod_1.z.number().positive().optional().default(30000), retries: zod_1.z.number().min(0).max(5).optional().default(3) }); // GitHub API Response Schemas const GitHubUserSchema = zod_1.z.object({ id: zod_1.z.union([zod_1.z.number(), zod_1.z.string()]), // Support both number and string IDs for compatibility login: zod_1.z.string(), name: zod_1.z.string().nullable(), email: zod_1.z.string().nullable(), avatar_url: zod_1.z.string().url(), type: zod_1.z.enum(['User', 'Organization']) }); const GitHubProjectV2Schema = zod_1.z.object({ id: zod_1.z.string(), number: zod_1.z.number(), title: zod_1.z.string(), url: zod_1.z.string().url(), public: zod_1.z.boolean(), owner: GitHubUserSchema, createdAt: zod_1.z.string(), updatedAt: zod_1.z.string() }); /** * GitHub API Client with authentication and error handling */ class GitHubAPIClient { constructor(config) { this.authenticated = false; this.user = null; this.config = GitHubConfigSchema.parse(config); } /** * Validate GitHub token and authenticate user */ async authenticate() { try { const response = await this.makeRequest('/user', 'GET'); if (!response.ok) { throw await this.createAPIError(response); } const userData = await response.json(); this.user = GitHubUserSchema.parse(userData); this.authenticated = true; return this.user; } catch (error) { this.authenticated = false; this.user = null; throw this.handleError(error, 'Failed to authenticate with GitHub API'); } } /** * Check if client is authenticated */ isAuthenticated() { return this.authenticated && this.user !== null; } /** * Get current authenticated user */ getCurrentUser() { return this.user; } /** * Validate token permissions for Projects API */ async validateProjectPermissions() { try { // Make a test request to check permissions const response = await this.makeRequest('/user', 'HEAD'); const scopes = response.headers.get('x-oauth-scopes')?.split(', ') || []; const acceptedScopes = response.headers.get('x-accepted-oauth-scopes')?.split(', ') || []; return { canReadProjects: scopes.includes('project') || scopes.includes('read:project'), canWriteProjects: scopes.includes('project'), scopes: scopes }; } catch (error) { throw this.handleError(error, 'Failed to validate project permissions'); } } /** * Execute GraphQL query for Projects v2 API */ async graphqlQuery(query, variables) { if (!this.authenticated) { throw new Error('Not authenticated. Call authenticate() first.'); } try { const response = await fetch(this.config.graphqlUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.token}`, 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'MCP-GitHub-Integration/1.0' }, body: JSON.stringify({ query, variables }), signal: AbortSignal.timeout(this.config.timeout) }); if (!response.ok) { throw await this.createAPIError(response); } const result = await response.json(); if (result.errors) { throw new Error(`GraphQL Error: ${result.errors.map((e) => e.message).join(', ')}`); } return result.data; } catch (error) { throw this.handleError(error, 'GraphQL query failed'); } } /** * Make REST API request with retry logic */ async makeRequest(endpoint, method = 'GET', body) { const url = `${this.config.baseUrl}${endpoint}`; let lastError = null; for (let attempt = 0; attempt <= this.config.retries; attempt++) { try { const response = await fetch(url, { method, headers: { 'Authorization': `Bearer ${this.config.token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'MCP-GitHub-Integration/1.0', ...(body && { 'Content-Type': 'application/json' }) }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(this.config.timeout) }); // Return successful responses or client errors (don't retry 4xx) if (response.ok || (response.status >= 400 && response.status < 500)) { return response; } // Server errors (5xx) - retry if (response.status >= 500 && attempt < this.config.retries) { await this.delay(Math.pow(2, attempt) * 1000); // Exponential backoff continue; } return response; } catch (error) { lastError = error; // Don't retry on network errors if this is the last attempt if (attempt === this.config.retries) { break; } // Wait before retrying await this.delay(Math.pow(2, attempt) * 1000); } } throw lastError || new Error('Request failed after all retries'); } /** * Create structured API error from response */ async createAPIError(response) { let message; let code; let documentation_url; try { const errorData = await response.json(); message = errorData.message || `GitHub API Error: ${response.status}`; code = errorData.code; documentation_url = errorData.documentation_url; } catch { message = `GitHub API Error: ${response.status} ${response.statusText}`; } const error = new Error(message); error.status = response.status; error.code = code; error.documentation_url = documentation_url; return error; } /** * Handle and format errors consistently */ handleError(error, context) { if (error instanceof Error) { // Create new error with context instead of modifying existing one const newError = new Error(`${context}: ${error.message}`); // Preserve stack trace if available newError.stack = error.stack; return newError; } return new Error(`${context}: ${String(error)}`); } /** * Utility delay function */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } exports.GitHubAPIClient = GitHubAPIClient; /** * GitHub Token Validator */ class GitHubTokenValidator { /** * Validate token format (basic pattern matching) */ static validateTokenFormat(token) { const issues = []; let type = 'unknown'; let description = ''; // Classic Personal Access Token (ghp_) if (token.startsWith('ghp_')) { type = 'classic_pat'; description = 'Classic Personal Access Token - Broad repository access with scopes'; if (token.length !== 40) { issues.push('Classic PAT should be 40 characters long'); } } // Fine-grained Personal Access Token (github_pat_) else if (token.startsWith('github_pat_')) { type = 'fine_grained_pat'; description = 'Fine-grained Personal Access Token - Repository-specific permissions'; if (token.length < 50) { issues.push('Fine-grained PAT appears to be too short'); } } // GitHub App token (ghs_) else if (token.startsWith('ghs_')) { type = 'app_token'; description = 'GitHub App Installation Token'; issues.push('GitHub App tokens are not currently supported'); } // Legacy token (no prefix) else if (/^[a-f0-9]{40}$/.test(token)) { type = 'legacy_token'; description = 'Legacy Personal Access Token (deprecated)'; issues.push('Legacy tokens are deprecated, use Personal Access Tokens'); } else { issues.push('Token format not recognized'); description = 'Unknown token format'; } return { valid: issues.length === 0, type, issues, description }; } /** * Validate token with live API call */ static async validateTokenWithAPI(token) { try { const client = new GitHubAPIClient({ token }); const user = await client.authenticate(); const permissions = await client.validateProjectPermissions(); return { valid: true, user, permissions }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : String(error) }; } } } exports.GitHubTokenValidator = GitHubTokenValidator; /** * MCP Tool Functions for GitHub Authentication */ // Tool: Authenticate with GitHub exports.authenticateGitHub = { name: 'github_authenticate', description: 'Authenticate with GitHub API using Personal Access Token', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'GitHub Personal Access Token (PAT) with project permissions' }, validateOnly: { type: 'boolean', description: 'If true, only validate token without storing authentication', default: false } }, required: ['token'] } }; // Tool: Check GitHub authentication status exports.checkGitHubAuth = { name: 'github_check_auth', description: 'Check current GitHub authentication status and permissions', inputSchema: { type: 'object', properties: {}, additionalProperties: false } }; // Tool: Validate GitHub token exports.validateGitHubToken = { name: 'github_validate_token', description: 'Validate GitHub token format and permissions without authenticating', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'GitHub token to validate' } }, required: ['token'] } }; // Global client instance (will be managed by MCP server) let globalGitHubClient = null; function getGitHubClient() { return globalGitHubClient; } function setGitHubClient(client) { globalGitHubClient = client; }