Skip to main content
Glama

Open Search MCP

by flyanima
MIT License
2
  • Apple
  • Linux
github-search-client.ts13.5 kB
import axios, { AxiosInstance } from 'axios'; import { Logger } from '../../utils/logger.js'; /** * GitHub 搜索客户端 - 专注于代码、仓库、用户搜索场景 * 支持仓库搜索、代码搜索、用户搜索、问题搜索 */ interface SearchOptions { sort?: 'stars' | 'forks' | 'updated' | 'created' | 'best-match'; order?: 'asc' | 'desc'; per_page?: number; page?: number; } interface RepositorySearchOptions extends SearchOptions { language?: string; size?: string; license?: string; } interface CodeSearchOptions extends SearchOptions { language?: string; filename?: string; extension?: string; } interface UserSearchOptions extends SearchOptions { type?: 'user' | 'org'; location?: string; language?: string; } interface GitHubRepository { id: number; name: string; full_name: string; description: string; html_url: string; clone_url: string; language: string; stargazers_count: number; forks_count: number; watchers_count: number; size: number; created_at: string; updated_at: string; pushed_at: string; owner: { login: string; avatar_url: string; html_url: string; }; topics: string[]; license?: { name: string; spdx_id: string; }; } interface GitHubUser { id: number; login: string; name: string; avatar_url: string; html_url: string; bio: string; company: string; location: string; email: string; public_repos: number; public_gists: number; followers: number; following: number; created_at: string; updated_at: string; } interface GitHubCodeResult { name: string; path: string; sha: string; url: string; html_url: string; repository: { id: number; name: string; full_name: string; html_url: string; description: string; language: string; stargazers_count: number; }; score: number; } interface SearchResult<T> { total_count: number; incomplete_results: boolean; items: T[]; } export class GitHubSearchClient { private httpClient: AxiosInstance; private logger: Logger; private requestCount = 0; private lastRequestTime = 0; private readonly REQUEST_DELAY = 1000; // 1秒延迟,避免速率限制 private readonly API_TOKEN: string; constructor() { this.logger = new Logger('GitHubSearch'); // 从环境变量获取API密钥 this.API_TOKEN = process.env.GITHUB_API_KEY || process.env.GITHUB_TOKEN || ''; this.httpClient = axios.create({ baseURL: 'https://api.github.com', timeout: 30000, headers: { 'Authorization': `Bearer ${this.API_TOKEN}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'Open-Search-MCP/2.0' } }); // 添加响应拦截器处理速率限制 this.httpClient.interceptors.response.use( response => response, error => { if (error.response?.status === 403 && error.response?.headers['x-ratelimit-remaining'] === '0') { const resetTime = error.response.headers['x-ratelimit-reset']; const waitTime = (parseInt(resetTime) * 1000) - Date.now(); this.logger.warn(`Rate limit exceeded. Reset in ${Math.ceil(waitTime / 1000)} seconds`); } throw error; } ); } /** * 通用API请求方法(带速率限制) */ private async makeRequest(endpoint: string, params: Record<string, any> = {}): Promise<any> { // 实施速率限制 const now = Date.now(); const timeSinceLastRequest = now - this.lastRequestTime; if (timeSinceLastRequest < this.REQUEST_DELAY) { const waitTime = this.REQUEST_DELAY - timeSinceLastRequest; this.logger.info(`Rate limiting: waiting ${waitTime}ms`); await new Promise(resolve => setTimeout(resolve, waitTime)); } this.requestCount++; this.lastRequestTime = Date.now(); try { const response = await this.httpClient.get(endpoint, { params }); // 记录速率限制信息 const remaining = response.headers['x-ratelimit-remaining']; const limit = response.headers['x-ratelimit-limit']; this.logger.info(`Rate limit: ${remaining}/${limit} remaining`); return response.data; } catch (error) { this.logger.error('GitHub API request failed:', error); throw error; } } /** * 搜索GitHub仓库 */ async searchRepositories(query: string, options: RepositorySearchOptions = {}): Promise<SearchResult<GitHubRepository>> { this.logger.info(`Searching repositories: ${query}`); // 构建搜索查询 let searchQuery = query; // 添加语言过滤 if (options.language) { searchQuery += ` language:${options.language}`; } // 添加大小过滤 if (options.size) { searchQuery += ` size:${options.size}`; } // 添加许可证过滤 if (options.license) { searchQuery += ` license:${options.license}`; } const params = { q: searchQuery, sort: options.sort || 'best-match', order: options.order || 'desc', per_page: Math.min(options.per_page || 10, 100), page: options.page || 1 }; const result = await this.makeRequest('/search/repositories', params); // 增强仓库信息 const enhancedItems = result.items.map((repo: GitHubRepository) => ({ ...repo, popularity_score: this.calculatePopularityScore(repo), activity_level: this.assessActivityLevel(repo), maintenance_status: this.assessMaintenanceStatus(repo) })); return { ...result, items: enhancedItems }; } /** * 搜索GitHub代码 */ async searchCode(query: string, options: CodeSearchOptions = {}): Promise<SearchResult<GitHubCodeResult>> { this.logger.info(`Searching code: ${query}`); // 构建搜索查询 let searchQuery = query; // 添加语言过滤 if (options.language) { searchQuery += ` language:${options.language}`; } // 添加文件名过滤 if (options.filename) { searchQuery += ` filename:${options.filename}`; } // 添加扩展名过滤 if (options.extension) { searchQuery += ` extension:${options.extension}`; } const params = { q: searchQuery, sort: options.sort || 'best-match', order: options.order || 'desc', per_page: Math.min(options.per_page || 10, 100), page: options.page || 1 }; const result = await this.makeRequest('/search/code', params); return result; } /** * 搜索GitHub用户 */ async searchUsers(query: string, options: UserSearchOptions = {}): Promise<SearchResult<GitHubUser>> { this.logger.info(`Searching users: ${query}`); // 构建搜索查询 let searchQuery = query; // 添加类型过滤 if (options.type) { searchQuery += ` type:${options.type}`; } // 添加位置过滤 if (options.location) { searchQuery += ` location:${options.location}`; } // 添加语言过滤 if (options.language) { searchQuery += ` language:${options.language}`; } const params = { q: searchQuery, sort: options.sort || 'best-match', order: options.order || 'desc', per_page: Math.min(options.per_page || 10, 100), page: options.page || 1 }; const result = await this.makeRequest('/search/users', params); // 增强用户信息 const enhancedItems = result.items.map((user: GitHubUser) => ({ ...user, influence_score: this.calculateInfluenceScore(user), activity_level: this.assessUserActivity(user) })); return { ...result, items: enhancedItems }; } /** * 获取仓库详情 */ async getRepository(owner: string, repo: string): Promise<GitHubRepository> { this.logger.info(`Getting repository: ${owner}/${repo}`); const result = await this.makeRequest(`/repos/${owner}/${repo}`); return { ...result, popularity_score: this.calculatePopularityScore(result), activity_level: this.assessActivityLevel(result), maintenance_status: this.assessMaintenanceStatus(result) }; } /** * 获取用户详情 */ async getUser(username: string): Promise<GitHubUser> { this.logger.info(`Getting user: ${username}`); const result = await this.makeRequest(`/users/${username}`); return { ...result, influence_score: this.calculateInfluenceScore(result), activity_level: this.assessUserActivity(result) }; } /** * 智能搜索 - 根据查询自动选择最佳方法 */ async smartSearch(query: string, options: any = {}): Promise<any> { this.logger.info(`Smart search: ${query}`); const intent = this.analyzeSearchIntent(query); switch (intent.type) { case 'repository': const repoResult = await this.searchRepositories(intent.query!, options); return { type: 'repository_search', query, result: repoResult }; case 'code': const codeResult = await this.searchCode(intent.query!, options); return { type: 'code_search', query, result: codeResult }; case 'user': const userResult = await this.searchUsers(intent.query!, options); return { type: 'user_search', query, result: userResult }; default: // 'general' // 默认搜索仓库 const generalResult = await this.searchRepositories(query, options); return { type: 'general_search', query, result: generalResult }; } } /** * 分析搜索意图 */ private analyzeSearchIntent(query: string): any { const normalizedQuery = query.toLowerCase().trim(); // 用户搜索 if (normalizedQuery.includes('user:') || normalizedQuery.includes('author:') || normalizedQuery.includes('developer') || normalizedQuery.includes('profile')) { return { type: 'user', query: normalizedQuery.replace(/user:|author:|developer|profile/g, '').trim() }; } // 代码搜索 if (normalizedQuery.includes('code:') || normalizedQuery.includes('function') || normalizedQuery.includes('class') || normalizedQuery.includes('method') || normalizedQuery.includes('filename:') || normalizedQuery.includes('extension:')) { return { type: 'code', query: normalizedQuery }; } // 仓库搜索 if (normalizedQuery.includes('repo:') || normalizedQuery.includes('repository') || normalizedQuery.includes('project')) { return { type: 'repository', query: normalizedQuery.replace(/repo:|repository|project/g, '').trim() }; } // 默认:仓库搜索 return { type: 'repository', query: normalizedQuery }; } /** * 计算仓库流行度分数 */ private calculatePopularityScore(repo: GitHubRepository): number { const stars = repo.stargazers_count || 0; const forks = repo.forks_count || 0; const watchers = repo.watchers_count || 0; // 加权计算流行度 return Math.round((stars * 0.5 + forks * 0.3 + watchers * 0.2) / 10); } /** * 评估仓库活跃度 */ private assessActivityLevel(repo: GitHubRepository): 'high' | 'medium' | 'low' { const lastPush = new Date(repo.pushed_at); const daysSinceLastPush = (Date.now() - lastPush.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceLastPush <= 7) return 'high'; if (daysSinceLastPush <= 30) return 'medium'; return 'low'; } /** * 评估仓库维护状态 */ private assessMaintenanceStatus(repo: GitHubRepository): 'active' | 'maintained' | 'stale' { const lastUpdate = new Date(repo.updated_at); const daysSinceUpdate = (Date.now() - lastUpdate.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceUpdate <= 30) return 'active'; if (daysSinceUpdate <= 180) return 'maintained'; return 'stale'; } /** * 计算用户影响力分数 */ private calculateInfluenceScore(user: GitHubUser): number { const repos = user.public_repos || 0; const followers = user.followers || 0; const following = user.following || 0; // 加权计算影响力 return Math.round((followers * 0.4 + repos * 0.3 + (followers / Math.max(following, 1)) * 0.3) / 10); } /** * 评估用户活跃度 */ private assessUserActivity(user: GitHubUser): 'high' | 'medium' | 'low' { const repos = user.public_repos || 0; const followers = user.followers || 0; if (repos > 50 && followers > 100) return 'high'; if (repos > 10 && followers > 10) return 'medium'; return 'low'; } /** * 获取使用统计 */ getUsageStats(): any { return { requestsUsed: this.requestCount, rateLimits: '5000 requests per hour (authenticated)', features: ['repository_search', 'code_search', 'user_search', 'smart_search'], lastRequestTime: this.lastRequestTime }; } /** * 验证API密钥 */ async validateApiKey(): Promise<boolean> { try { await this.makeRequest('/user'); return true; } catch (error) { return false; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/flyanima/open-search-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server