Skip to main content
Glama

Open Search MCP

by flyanima
MIT License
2
  • Apple
  • Linux
stackexchange-client.ts13.4 kB
import axios, { AxiosInstance } from 'axios'; import { Logger } from '../../utils/logger.js'; /** * Stack Exchange API 客户端 - 专注于编程问答和技术支持 * 支持Stack Overflow、Super User、Ask Ubuntu等技术社区搜索 */ interface SearchOptions { site?: string; sort?: 'activity' | 'votes' | 'creation' | 'relevance' | 'hot' | 'reputation'; order?: 'desc' | 'asc'; tagged?: string; nottagged?: string; intitle?: string; user?: string; min?: number; max?: number; fromdate?: number; todate?: number; page?: number; pagesize?: number; } interface StackExchangeQuestion { question_id: number; title: string; body?: string; link: string; score: number; view_count: number; answer_count: number; comment_count: number; creation_date: number; last_activity_date: number; last_edit_date?: number; is_answered: boolean; accepted_answer_id?: number; tags: string[]; owner: { user_id?: number; display_name?: string; reputation?: number; user_type?: string; profile_image?: string; link?: string; }; closed_date?: number; closed_reason?: string; } interface StackExchangeAnswer { answer_id: number; question_id: number; body?: string; link?: string; score: number; is_accepted: boolean; creation_date: number; last_activity_date: number; last_edit_date?: number; comment_count: number; owner: { user_id?: number; display_name?: string; reputation?: number; user_type?: string; profile_image?: string; link?: string; }; } interface StackExchangeUser { user_id: number; display_name: string; reputation: number; user_type: string; profile_image?: string; website_url?: string; location?: string; about_me?: string; view_count: number; up_vote_count: number; down_vote_count: number; question_count: number; answer_count: number; creation_date: number; last_access_date: number; link: string; badge_counts?: { bronze: number; silver: number; gold: number; }; } interface SearchResponse<T> { items: T[]; has_more: boolean; quota_max: number; quota_remaining: number; page?: number; page_size?: number; total?: number; type?: string; } export class StackExchangeClient { private httpClient: AxiosInstance; private logger: Logger; private requestCount = 0; private lastRequestTime = 0; private readonly REQUEST_DELAY = 100; // 100ms延迟,Stack Exchange API相对宽松 private readonly DEFAULT_SITE = 'stackoverflow'; constructor() { this.logger = new Logger('StackExchange'); this.httpClient = axios.create({ baseURL: 'https://api.stackexchange.com/2.3', timeout: 30000, headers: { 'User-Agent': 'Open-Search-MCP/2.0' } }); // 添加响应拦截器处理压缩和错误 this.httpClient.interceptors.response.use( response => response, error => { if (error.response?.status === 429) { this.logger.warn('Rate limit exceeded for Stack Exchange API'); } else if (error.response?.status === 400) { this.logger.warn('Bad request to Stack Exchange API'); } 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; await new Promise(resolve => setTimeout(resolve, waitTime)); } this.requestCount++; this.lastRequestTime = Date.now(); try { const requestParams = { site: this.DEFAULT_SITE, pagesize: 30, ...params }; const response = await this.httpClient.get(endpoint, { params: requestParams }); return response.data; } catch (error) { this.logger.error('Stack Exchange API request failed:', error); throw error; } } /** * 搜索问题 */ async searchQuestions(query: string, options: SearchOptions = {}): Promise<SearchResponse<StackExchangeQuestion>> { this.logger.info(`Searching questions: ${query}`); const params = { order: 'desc', sort: 'relevance', intitle: query, site: options.site || this.DEFAULT_SITE, pagesize: Math.min(options.pagesize || 30, 100), page: options.page || 1, tagged: options.tagged, nottagged: options.nottagged, min: options.min, max: options.max, fromdate: options.fromdate, todate: options.todate, filter: 'withbody' // 包含问题内容 }; // 移除undefined值 Object.keys(params).forEach(key => { if (params[key as keyof typeof params] === undefined) { delete params[key as keyof typeof params]; } }); return await this.makeRequest('/search', params); } /** * 高级搜索问题 */ async advancedSearch(query: string, options: SearchOptions = {}): Promise<SearchResponse<StackExchangeQuestion>> { this.logger.info(`Advanced search: ${query}`); const params = { order: options.order || 'desc', sort: options.sort || 'relevance', q: query, site: options.site || this.DEFAULT_SITE, pagesize: Math.min(options.pagesize || 30, 100), page: options.page || 1, tagged: options.tagged, nottagged: options.nottagged, min: options.min, max: options.max, fromdate: options.fromdate, todate: options.todate, filter: 'withbody' }; // 移除undefined值 Object.keys(params).forEach(key => { if (params[key as keyof typeof params] === undefined) { delete params[key as keyof typeof params]; } }); return await this.makeRequest('/search/advanced', params); } /** * 获取问题详情 */ async getQuestion(questionId: number, site: string = this.DEFAULT_SITE): Promise<StackExchangeQuestion | null> { this.logger.info(`Getting question: ${questionId}`); const params = { site, filter: 'withbody' }; const response = await this.makeRequest(`/questions/${questionId}`, params); return response.items?.[0] || null; } /** * 获取问题的答案 */ async getAnswers(questionId: number, site: string = this.DEFAULT_SITE): Promise<SearchResponse<StackExchangeAnswer>> { this.logger.info(`Getting answers for question: ${questionId}`); const params = { site, order: 'desc', sort: 'votes', filter: 'withbody', pagesize: 30 }; return await this.makeRequest(`/questions/${questionId}/answers`, params); } /** * 搜索用户 */ async searchUsers(query: string, site: string = this.DEFAULT_SITE): Promise<SearchResponse<StackExchangeUser>> { this.logger.info(`Searching users: ${query}`); const params = { inname: query, site, order: 'desc', sort: 'reputation', pagesize: 30 }; return await this.makeRequest('/users', params); } /** * 获取用户详情 */ async getUser(userId: number, site: string = this.DEFAULT_SITE): Promise<StackExchangeUser | null> { this.logger.info(`Getting user: ${userId}`); const params = { site }; const response = await this.makeRequest(`/users/${userId}`, params); return response.items?.[0] || null; } /** * 获取热门问题 */ async getHotQuestions(site: string = this.DEFAULT_SITE): Promise<SearchResponse<StackExchangeQuestion>> { this.logger.info(`Getting hot questions from: ${site}`); const params = { site, order: 'desc', sort: 'hot', pagesize: 30, filter: 'withbody' }; return await this.makeRequest('/questions', params); } /** * 按标签搜索 */ async searchByTag(tag: string, site: string = this.DEFAULT_SITE, options: SearchOptions = {}): Promise<SearchResponse<StackExchangeQuestion>> { this.logger.info(`Searching by tag: ${tag}`); const params = { site, order: options.order || 'desc', sort: options.sort || 'votes', tagged: tag, pagesize: Math.min(options.pagesize || 30, 100), page: options.page || 1, filter: 'withbody' }; return await this.makeRequest('/questions', params); } /** * 获取网站信息 */ async getSites(): Promise<any> { this.logger.info('Getting Stack Exchange sites'); const params = { pagesize: 100, filter: 'default' }; return await this.makeRequest('/sites', params); } /** * 智能搜索 - 根据查询自动选择最佳搜索方法 */ async smartSearch(query: string, options: SearchOptions = {}): Promise<any> { this.logger.info(`Smart search: ${query}`); const intent = this.analyzeSearchIntent(query); let searchResult: SearchResponse<StackExchangeQuestion>; switch (intent.type) { case 'tag': searchResult = await this.searchByTag(intent.tag!, options.site, options); break; case 'user': const userResult = await this.searchUsers(intent.query, options.site); return { type: intent.type, query, originalQuery: query, intent: intent, result: userResult }; case 'advanced': searchResult = await this.advancedSearch(intent.query, options); break; default: // 'simple' searchResult = await this.searchQuestions(query, options); break; } return { type: intent.type, query, originalQuery: query, processedQuery: intent.query, intent: intent, result: searchResult }; } /** * 分析搜索意图 */ private analyzeSearchIntent(query: string): any { const normalizedQuery = query.toLowerCase().trim(); // 标签搜索意图 if (normalizedQuery.startsWith('[') && normalizedQuery.includes(']')) { const tagMatch = normalizedQuery.match(/\[([^\]]+)\]/); if (tagMatch) { return { type: 'tag', tag: tagMatch[1], query: normalizedQuery.replace(/\[[^\]]+\]/g, '').trim(), confidence: 0.9 }; } } // 用户搜索意图 if (normalizedQuery.includes('user:') || normalizedQuery.includes('by ')) { const userQuery = normalizedQuery.replace(/user:|by /g, '').trim(); return { type: 'user', query: userQuery, confidence: 0.85 }; } // 高级搜索意图(包含多个关键词或特殊字符) if (normalizedQuery.includes(' AND ') || normalizedQuery.includes(' OR ') || normalizedQuery.includes('"') || normalizedQuery.split(' ').length > 3) { return { type: 'advanced', query: normalizedQuery, confidence: 0.8 }; } // 默认:简单搜索 return { type: 'simple', query: normalizedQuery, confidence: 0.7 }; } /** * 获取使用统计 */ getUsageStats(): any { return { requestsUsed: this.requestCount, rateLimits: '10,000 requests per day (no key required)', features: ['question_search', 'answer_search', 'user_search', 'tag_search', 'hot_questions'], lastRequestTime: this.lastRequestTime, defaultSite: this.DEFAULT_SITE }; } /** * 验证API配置 */ async validateConfig(): Promise<boolean> { try { const testResult = await this.searchQuestions('test', { pagesize: 1 }); return testResult.items && testResult.items.length >= 0; } catch (error) { return false; } } /** * 获取支持的网站 */ getSupportedSites(): Record<string, string> { return { 'stackoverflow': 'Stack Overflow - Programming Q&A', 'superuser': 'Super User - Computer enthusiasts and power users', 'serverfault': 'Server Fault - System and network administrators', 'askubuntu': 'Ask Ubuntu - Ubuntu users and developers', 'mathoverflow.net': 'MathOverflow - Professional mathematicians', 'tex': 'TeX - LaTeX Stack Exchange', 'dba': 'Database Administrators Stack Exchange', 'webmasters': 'Webmasters Stack Exchange', 'gamedev': 'Game Development Stack Exchange', 'security': 'Information Security Stack Exchange' }; } /** * 获取支持的搜索类型 */ getSupportedSearchTypes(): Record<string, string> { return { 'questions': 'Search questions by title and content', 'advanced': 'Advanced search with multiple criteria', 'tags': 'Search questions by specific tags', 'users': 'Search users by name and reputation', 'answers': 'Search answers for specific questions', 'hot': 'Get currently trending questions' }; } /** * 获取支持的排序选项 */ getSupportedSortOptions(): Record<string, string> { return { 'relevance': 'Most relevant to search query', 'votes': 'Highest voted first', 'activity': 'Most recently active', 'creation': 'Most recently created', 'hot': 'Currently trending' }; } }

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