Skip to main content
Glama
server.test.ts13.3 kB
/** * Stellaris Modding Helper MCP Server のテスト * 実際のAPIを使用したインテグレーションテスト * * 注意: このテストは実際のGitHubおよびSteamCMD APIにリクエストを送信します。 * APIのレート制限を避けるため、テストの実行には時間がかかる場合があります。 * そのため、各テストケースのタイムアウトを30秒に設定しています。 */ import { beforeEach, describe, expect, it } from "vitest"; // server.tsから複製・流用する定数とヘルパー関数 const CWTOOLS_REPO_OWNER = "cwtools"; const CWTOOLS_REPO_NAME = "cwtools-stellaris-config"; const GITHUB_API_BASE = "https://api.github.com"; const STELLARIS_APP_ID = "281990"; const STEAMCMD_API_BASE = "https://api.steamcmd.net/v1"; const STELLARIS_DOCS_OWNER = "OldEnt"; const STELLARIS_DOCS_REPO = "stellaris-triggers-modifiers-effects-list"; const RETRY_CONFIG = { backoffMultiplier: 2, baseDelay: 1000, // 1秒 maxDelay: 10000, // 10秒 maxRetries: 3, }; /** * 指数バックオフを使用したリトライ機能付きfetch */ async function fetchWithRetry( url: string, options?: RequestInit, retryCount = 0, ): Promise<Response> { try { const response = await fetch(url, options); if (response.status === 403 && response.statusText.includes("rate limit")) { if (retryCount < RETRY_CONFIG.maxRetries) { const delay = Math.min( RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, retryCount), RETRY_CONFIG.maxDelay, ); console.log( `Rate limit hit, retrying in ${delay}ms (attempt ${retryCount + 1}/${RETRY_CONFIG.maxRetries})`, ); await new Promise((resolve) => setTimeout(resolve, delay)); return fetchWithRetry(url, options, retryCount + 1); } } if (response.status >= 500 && response.status < 600) { if (retryCount < RETRY_CONFIG.maxRetries) { const delay = Math.min( RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, retryCount), RETRY_CONFIG.maxDelay, ); console.log( `Server error ${response.status}, retrying in ${delay}ms (attempt ${ retryCount + 1 }/${RETRY_CONFIG.maxRetries})`, ); await new Promise((resolve) => setTimeout(resolve, delay)); return fetchWithRetry(url, options, retryCount + 1); } } return response; } catch (error) { if (retryCount < RETRY_CONFIG.maxRetries) { const delay = Math.min( RETRY_CONFIG.baseDelay * Math.pow(RETRY_CONFIG.backoffMultiplier, retryCount), RETRY_CONFIG.maxDelay, ); console.log( `Network error, retrying in ${delay}ms (attempt ${retryCount + 1}/${RETRY_CONFIG.maxRetries}):`, error instanceof Error ? error.message : String(error), ); await new Promise((resolve) => setTimeout(resolve, delay)); return fetchWithRetry(url, options, retryCount + 1); } throw error; } } /** * SteamCMD APIからアプリ情報を取得する */ async function fetchSteamAppInfo(appId: string): Promise<unknown> { const response = await fetchWithRetry(`${STEAMCMD_API_BASE}/info/${appId}`); if (!response.ok) { throw new Error( `Failed to fetch app info: ${response.status} ${response.statusText}`, ); } const data = (await response.json()) as { data: Record<string, unknown>; status: string; }; if (data.status !== "success") { throw new Error(`API returned error: ${JSON.stringify(data.data)}`); } return data.data[appId]; } /** * GitHub APIから最新のタグを取得する */ async function getLatestTag(owner: string, repo: string): Promise<string> { const response = await fetchWithRetry( `${GITHUB_API_BASE}/repos/${owner}/${repo}/tags`, ); if (!response.ok) { throw new Error( `Failed to fetch tags: ${response.status} ${response.statusText}`, ); } const tags = (await response.json()) as Array<{ name: string }>; if (tags.length === 0) { throw new Error("No tags found in repository"); } return tags[0].name; } /** * GitHub APIからディレクトリの内容を取得する */ async function getDirectoryContents( owner: string, repo: string, path: string, ref?: string, ): Promise< Array<{ download_url: string; name: string; path: string; size: number; type: string; }> > { const url = new URL( `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${path}`, ); if (ref) { url.searchParams.set("ref", ref); } const response = await fetchWithRetry(url.toString()); if (!response.ok) { throw new Error( `Failed to fetch directory contents: ${response.status} ${response.statusText}`, ); } const contents = (await response.json()) as Array<{ download_url: string; name: string; path: string; size: number; type: string; }>; if (!Array.isArray(contents)) { throw new Error("Expected directory contents to be an array"); } return contents; } /** * GitHub APIからファイルの内容を取得する */ async function getFileContent( owner: string, repo: string, path: string, ref?: string, ): Promise<string> { const url = new URL( `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${path}`, ); if (ref) { url.searchParams.set("ref", ref); } const response = await fetchWithRetry(url.toString()); if (!response.ok) { throw new Error( `Failed to fetch file content: ${response.status} ${response.statusText}`, ); } const fileData = (await response.json()) as { content: string; encoding: string; }; if (fileData.encoding === "base64") { return Buffer.from(fileData.content, "base64").toString("utf-8"); } return fileData.content; } /** * リポジトリのファイル一覧を取得し、利用可能なバージョン一覧を取得する(降順ソート済み) */ async function getAvailableStellarisVersions( owner: string, repo: string, ): Promise<string[]> { const response = await fetchWithRetry( `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents`, ); if (!response.ok) { throw new Error( `Failed to fetch repository contents: ${response.status} ${response.statusText}`, ); } const contents = (await response.json()) as Array<{ name: string; type: string; }>; const versionFiles = contents .filter((item) => item.type === "file") .map((item) => item.name) .filter((name) => /^\d+\.\d+\.\d+_game_/.test(name)) .map((name) => { const match = name.match(/^(\d+\.\d+\.\d+)_game_/); return match ? match[1] : null; }) .filter((version): version is string => version !== null); const uniqueVersions = [...new Set(versionFiles)]; uniqueVersions.sort((a, b) => { const aVersion = a.split(".").map((n) => parseInt(n) || 0); const bVersion = b.split(".").map((n) => parseInt(n) || 0); for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) { const diff = (bVersion[i] || 0) - (aVersion[i] || 0); if (diff !== 0) return diff; } return 0; }); return uniqueVersions; } describe("Stellaris Modding Helper - Integration Tests", () => { beforeEach(async () => { // APIレート制限を避けるためにテスト間に待機時間を設ける await new Promise((resolve) => setTimeout(resolve, 1500)); }); describe("Tool: stellaris-version", () => { it( "should fetch and format Stellaris version info from SteamCMD API", async () => { const appInfo = (await fetchSteamAppInfo(STELLARIS_APP_ID)) as any; expect(appInfo).toBeDefined(); expect(appInfo.common.name).toBe("Stellaris"); expect(appInfo.depots.branches.public).toBeDefined(); expect(appInfo.extended.developer).toBe("Paradox Development Studio"); }, { timeout: 30000 }, ); }); describe("Tool: cwtools-config", () => { it( "should list all config files including subdirectories", async () => { const configContents = await getDirectoryContents( CWTOOLS_REPO_OWNER, CWTOOLS_REPO_NAME, "config", ); const subdirectories = configContents.filter( (item) => item.type === "dir", ); expect(subdirectories.length).toBeGreaterThan(0); const subdirContents = await getDirectoryContents( CWTOOLS_REPO_OWNER, CWTOOLS_REPO_NAME, `config/${subdirectories[0].name}`, ); const cwtFiles = subdirContents.filter( (item) => item.type === "file" && item.name.endsWith(".cwt"), ); expect(cwtFiles.length).toBeGreaterThan(0); }, { timeout: 30000 }, ); it( "should fetch a specific file from a subdirectory", async () => { const fileContent = await getFileContent( CWTOOLS_REPO_OWNER, CWTOOLS_REPO_NAME, "config/common/buildings.cwt", ); expect(fileContent).toBeTruthy(); expect(fileContent).toContain("type[building]"); }, { timeout: 30000 }, ); }); describe("Tool: stellaris-search", () => { it( 'should find keyword "has_technology" and return snippets', async () => { const versions = await getAvailableStellarisVersions( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, ); const latestVersion = versions[0]; const fileName = `${latestVersion}_game_triggers.log`; const fileContent = await getFileContent( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, fileName, ); const lines = fileContent.split("\n"); const results = lines .map((line, i) => ({ line, i })) .filter(({ line }) => line.toLowerCase().includes("has_technology")); expect(results.length).toBeGreaterThan(0); expect(results[0].line).toContain("has_technology"); }, { timeout: 30000 }, ); it( 'should return "not found" message for a non-existing keyword', async () => { const versions = await getAvailableStellarisVersions( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, ); const latestVersion = versions[0]; const fileName = `${latestVersion}_game_triggers.log`; const fileContent = await getFileContent( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, fileName, ); const lines = fileContent.split("\n"); const results = lines.filter((line) => line.toLowerCase().includes("non_existent_keyword_xyz"), ); expect(results.length).toBe(0); }, { timeout: 30000 }, ); }); describe("Tool: stellaris-validate", () => { it( 'should return VALID for an existing element "has_technology"', async () => { const versions = await getAvailableStellarisVersions( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, ); const latestVersion = versions[0]; const fileName = `${latestVersion}_game_triggers.log`; const fileContent = await getFileContent( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, fileName, ); const element = "has_technology"; const exactMatch = new RegExp(`\\b${element}\\b`, "i").test(fileContent); expect(exactMatch).toBe(true); }, { timeout: 30000 }, ); it( 'should return NOT FOUND for a non-existing element "non_existent_element_xyz"', async () => { const versions = await getAvailableStellarisVersions( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, ); const latestVersion = versions[0]; const fileName = `${latestVersion}_game_triggers.log`; const fileContent = await getFileContent( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, fileName, ); const element = "non_existent_element_xyz"; const exactMatch = new RegExp(`\\b${element}\\b`, "i").test(fileContent); expect(exactMatch).toBe(false); }, { timeout: 30000 }, ); it( 'should return PARTIAL MATCH for a similar element "technolog"', async () => { const versions = await getAvailableStellarisVersions( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, ); const latestVersion = versions[0]; const fileName = `${latestVersion}_game_triggers.log`; const fileContent = await getFileContent( STELLARIS_DOCS_OWNER, STELLARIS_DOCS_REPO, fileName, ); const element = "technolog"; // 存在しないであろう単語に変更 const exactMatch = new RegExp(`\\b${element}\\b`, "i").test(fileContent); const partialMatch = fileContent.toLowerCase().includes(element); expect(exactMatch).toBe(false); // exact matchでは見つからない expect(partialMatch).toBe(true); // partial matchでは見つかる (例: "technology" が含まれる行) }, { timeout: 30000 }, ); }); });

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/Stellaris-Modding-MCP-Server'

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