Skip to main content
Glama
kongyo2

EVE University Wiki MCP Server

eve-wiki-client.ts15.4 kB
import axios, { AxiosInstance } from "axios"; import * as cheerio from "cheerio"; export interface Article { content: string; pageid: number; revid: number; timestamp: string; title: string; } export interface SearchResult { pageid: number; snippet: string; timestamp: string; title: string; wordcount: number; } export interface Section { content?: string; index: number; level: number; title: string; } export interface WaybackSnapshot { timestamp: string; url: string; available: boolean; } export class EveWikiClient { private baseUrl: string; private client: AxiosInstance; private waybackClient: AxiosInstance; private maxRetries: number; private retryDelay: number; constructor(maxRetries: number = 3, retryDelay: number = 1000) { this.baseUrl = "https://wiki.eveuniversity.org/api.php"; this.maxRetries = maxRetries; this.retryDelay = retryDelay; this.client = axios.create({ baseURL: this.baseUrl, headers: { "User-Agent": "EVE-University-MCP-Server/1.0.0", }, timeout: 30000, }); // Wayback Machine client for fallback this.waybackClient = axios.create({ headers: { "User-Agent": "EVE-University-MCP-Server/1.0.0", }, timeout: 30000, }); } private async retryableRequest<T>(requestFn: () => Promise<T>): Promise<T> { let lastError: Error; for (let i = 0; i <= this.maxRetries; i++) { try { return await requestFn(); } catch (error) { lastError = error as Error; // 最後のリトライでも失敗した場合、エラーをスロー if (i === this.maxRetries) { throw lastError; } // 次のリトライ前に遅延 await new Promise(resolve => setTimeout(resolve, this.retryDelay * Math.pow(2, i))); } } throw lastError!; } /** * Check if a URL is available in Wayback Machine */ private async checkWaybackAvailability(url: string): Promise<WaybackSnapshot | null> { try { const response = await this.waybackClient.get("https://archive.org/wayback/available", { params: { url } }); if (response?.data?.archived_snapshots?.closest?.available) { return { timestamp: response.data.archived_snapshots.closest.timestamp, url: response.data.archived_snapshots.closest.url, available: true }; } return null; } catch (error) { console.warn("Wayback Machine availability check failed:", error); return null; } } /** * Get archived content from Wayback Machine */ private async getWaybackContent(url: string, timestamp?: string): Promise<string> { try { let waybackUrl: string; if (timestamp) { waybackUrl = `https://web.archive.org/web/${timestamp}id_/${url}`; } else { // Get the latest available snapshot const snapshot = await this.checkWaybackAvailability(url); if (!snapshot) { throw new Error("No archived version available"); } waybackUrl = snapshot.url.replace(/\/web\/\d+\//, `/web/${snapshot.timestamp}id_/`); } const response = await this.waybackClient.get(waybackUrl, { responseType: 'text' }); return response.data; } catch (error) { throw new Error(`Failed to retrieve from Wayback Machine: ${error}`); } } /** * Extract text content from HTML using cheerio */ private extractTextFromHtml(html: string): string { try { const $ = cheerio.load(html); // Remove script and style elements $('script, style, nav, header, footer, .mw-navigation').remove(); // Get main content area const mainContent = $('#mw-content-text, .mw-parser-output, #content, main').first(); if (mainContent.length > 0) { return mainContent.text().trim(); } // Fallback to body content return $('body').text().trim(); } catch (error) { console.warn("Failed to parse HTML:", error); return html; } } /** * Get full article content with Wayback Machine fallback */ async getArticle(title: string): Promise<Article> { return this.retryableRequest(async () => { try { const response = await this.client.get("", { params: { action: "query", format: "json", prop: "revisions", rvprop: "content|timestamp|ids", rvslots: "main", titles: title, }, }); const pages = response.data?.query?.pages; if (!pages) { throw new Error("No pages found"); } const pageId = Object.keys(pages)[0]; const page = pages[pageId]; if (page.missing) { throw new Error(`Article "${title}" not found`); } const revision = page.revisions?.[0]; if (!revision) { throw new Error("No revision found"); } return { content: revision.slots?.main?.["*"] || "", pageid: page.pageid, revid: revision.revid, timestamp: revision.timestamp, title: page.title, }; } catch (error) { console.error("Primary EVE Wiki request failed, trying Wayback Machine fallback:", error); // Try Wayback Machine fallback try { const articleUrl = `https://wiki.eveuniversity.org/wiki/${encodeURIComponent(title.replace(/ /g, '_'))}`; const waybackContent = await this.getWaybackContent(articleUrl); const textContent = this.extractTextFromHtml(waybackContent); return { content: textContent, pageid: -1, // Indicate this is from Wayback Machine revid: -1, timestamp: new Date().toISOString(), title: `${title} (Archived)`, }; } catch (waybackError) { console.error("Wayback Machine fallback also failed:", waybackError); throw new Error(`Failed to get article "${title}" from both primary source and Wayback Machine`); } } }); } /** * Get links from an article */ async getLinks(title: string): Promise<string[]> { return this.retryableRequest(async () => { try { const response = await this.client.get("", { params: { action: "query", format: "json", pllimit: 500, prop: "links", titles: title, }, }); const pages = response.data?.query?.pages; if (!pages) { return []; } const pageId = Object.keys(pages)[0]; const page = pages[pageId]; if (page.missing || !page.links) { return []; } return page.links.map((link: { title: string }) => link.title); } catch (error) { console.error("Error getting links:", error); throw new Error(`Failed to get links for "${title}": ${error}`); } }); } /** * Get related topics based on categories */ async getRelatedTopics(title: string, limit: number = 10): Promise<string[]> { return this.retryableRequest(async () => { try { // First get categories for the article const categoriesResponse = await this.client.get("", { params: { action: "query", cllimit: 10, format: "json", prop: "categories", titles: title, }, }); const pages = categoriesResponse.data?.query?.pages; if (!pages) { return []; } const pageId = Object.keys(pages)[0]; const page = pages[pageId]; if (page.missing || !page.categories) { return []; } // Get articles from the same categories const categories = page.categories.slice(0, 3); // Limit to first 3 categories const relatedArticles: Set<string> = new Set(); for (const category of categories) { try { const categoryResponse = await this.client.get("", { params: { action: "query", cmlimit: 5, cmtitle: category.title, cmtype: "page", format: "json", list: "categorymembers", }, }); if (categoryResponse.data?.query?.categorymembers) { categoryResponse.data.query.categorymembers.forEach( (member: { title: string }) => { if (member.title !== title && relatedArticles.size < limit) { relatedArticles.add(member.title); } }, ); } } catch (error) { console.warn( `Error getting category members for ${category.title}:`, error, ); } } return Array.from(relatedArticles); } catch (error) { console.error("Error getting related topics:", error); throw new Error(`Failed to get related topics for "${title}": ${error}`); } }); } /** * Get article sections */ async getSections(title: string): Promise<Section[]> { return this.retryableRequest(async () => { try { const response = await this.client.get("", { params: { action: "parse", format: "json", page: title, prop: "sections", }, }); if (response.data?.parse?.sections) { return response.data.parse.sections.map( (section: { index: string; level: string; line: string }) => ({ index: parseInt(section.index) || 0, level: parseInt(section.level) || 1, title: section.line, }), ); } return []; } catch (error) { console.error("Error getting sections:", error); throw new Error(`Failed to get sections for "${title}": ${error}`); } }); } /** * Get article summary with Wayback Machine fallback */ async getSummary(title: string): Promise<string> { return this.retryableRequest(async () => { try { const response = await this.client.get("", { params: { action: "query", exintro: true, explaintext: true, exsectionformat: "plain", format: "json", prop: "extracts", titles: title, }, }); const pages = response.data?.query?.pages; if (!pages) { throw new Error("No pages found"); } const pageId = Object.keys(pages)[0]; const page = pages[pageId]; if (page.missing) { throw new Error(`Article "${title}" not found`); } return page.extract || "No summary available"; } catch (error) { console.error("Primary EVE Wiki summary request failed, trying Wayback Machine fallback:", error); // Try Wayback Machine fallback try { const articleUrl = `https://wiki.eveuniversity.org/wiki/${encodeURIComponent(title.replace(/ /g, '_'))}`; const waybackContent = await this.getWaybackContent(articleUrl); const textContent = this.extractTextFromHtml(waybackContent); // Extract first paragraph as summary const paragraphs = textContent.split('\n\n').filter(p => p.trim().length > 0); const summary = paragraphs[0] || textContent.substring(0, 500); return `${summary} (Retrieved from archived version)`; } catch (waybackError) { console.error("Wayback Machine fallback also failed:", waybackError); throw new Error(`Failed to get summary for "${title}" from both primary source and Wayback Machine`); } } }); } /** * Search for articles on EVE University Wiki with Wayback Machine fallback */ async search(query: string, limit: number = 10): Promise<SearchResult[]> { return this.retryableRequest(async () => { try { const response = await this.client.get("", { params: { action: "query", format: "json", list: "search", srlimit: limit, srprop: "snippet|titlesnippet|size|wordcount|timestamp", srsearch: query, }, }); if (response.data?.query?.search) { return response.data.query.search.map( (item: { pageid: number; snippet?: string; timestamp?: string; title: string; wordcount?: number; }) => ({ pageid: item.pageid, snippet: this.cleanSnippet(item.snippet || ""), timestamp: item.timestamp || "", title: item.title, wordcount: item.wordcount || 0, }), ); } return []; } catch (error) { console.error("Primary EVE Wiki search failed, trying Wayback Machine fallback:", error); // Try Wayback Machine fallback with common EVE-related pages try { const commonEvePages = [ "Fitting", "Ships", "Mining", "Trading", "PvP", "Missions", "Corporations", "Alliances", "Industry", "Exploration" ]; const matchingPages = commonEvePages.filter(page => page.toLowerCase().includes(query.toLowerCase()) || query.toLowerCase().includes(page.toLowerCase()) ); if (matchingPages.length === 0) { // If no matches, try to get a few common pages matchingPages.push(...commonEvePages.slice(0, Math.min(limit, 3))); } const results: SearchResult[] = []; for (const page of matchingPages.slice(0, limit)) { try { const articleUrl = `https://wiki.eveuniversity.org/wiki/${encodeURIComponent(page.replace(/ /g, '_'))}`; const snapshot = await this.checkWaybackAvailability(articleUrl); if (snapshot) { results.push({ pageid: -1, // Indicate this is from Wayback Machine snippet: `Archived content related to ${page} (from Wayback Machine)`, timestamp: snapshot.timestamp, title: `${page} (Archived)`, wordcount: 0, }); } } catch (pageError) { console.warn(`Failed to check Wayback availability for ${page}:`, pageError); } } return results; } catch (waybackError) { console.error("Wayback Machine fallback also failed:", waybackError); throw new Error(`Failed to search EVE Wiki from both primary source and Wayback Machine`); } } }); } /** * Clean HTML snippets from search results */ private cleanSnippet(snippet: string): string { // Remove HTML tags and decode entities const $ = cheerio.load(snippet); return $.root().text().trim(); } }

Implementation Reference

Latest Blog Posts

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/kongyo2/EVE-University-Wiki-MCP-Server'

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