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
JavaScript
;
/**
* 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;
}