server.ts•37.7 kB
/**
* Stellaris Modding MCP Server
* ステラリスのmodding用MCPサーバー
* SteamCMD APIを使用してステラリスの最新バージョン情報を取得する
*/
import { FastMCP } from "fastmcp";
import { z } from "zod";
const server = new FastMCP({
instructions: `ステラリスのmodding支援ツール。以下の機能を提供します:
1. **ゲームバージョン情報** (stellaris-version): SteamCMD APIからステラリスの最新バージョン、ビルドID、更新日時、利用可能なバージョン一覧を取得
2. **CWToolsコンフィグ** (cwtools-config): cwtools/cwtools-stellaris-configリポジトリからStellarisのmodding検証ルール(.cwt files)を取得。安定版/開発版の選択、特定ファイルまたは全ファイル一覧の取得が可能
3. **ドキュメント検索** (stellaris-search): OldEnt/stellaris-triggers-modifiers-effects-listリポジトリから最新バージョンのドキュメントを検索し、指定したキーワードに一致する箇所をコードスニペットとして返す
4. **要素検証** (stellaris-validate): 指定したtrigger、effect、modifier、scope、localizationが最新バージョンで有効かどうかをチェック
全ての機能にリトライ機能とエラーハンドリングが組み込まれており、安定した動作を提供します。`,
name: "Stellaris Modding Helper",
version: "1.0.0",
});
// ステラリスのApp ID
const STELLARIS_APP_ID = "281990";
const STEAMCMD_API_BASE = "https://api.steamcmd.net/v1";
// CWTools config repository
const CWTOOLS_REPO_OWNER = "cwtools";
const CWTOOLS_REPO_NAME = "cwtools-stellaris-config";
const GITHUB_API_BASE = "https://api.github.com";
// OldEnt Stellaris documentation repository
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,
};
/**
* 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];
}
/**
* 指数バックオフを使用したリトライ機能付き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;
}
}
/**
* Unix timestampを読みやすい日付形式に変換
*/
function formatTimestamp(timestamp: number | string): string {
const date = new Date(Number(timestamp) * 1000);
return date.toLocaleString("ja-JP", {
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
second: "2-digit",
timeZone: "Asia/Tokyo",
year: "numeric",
});
}
/**
* リポジトリのファイル一覧を取得し、利用可能なバージョン一覧を取得する(降順ソート済み)
*/
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;
}
/**
* GitHub APIからディレクトリの内容を取得する
*/
async function getDirectoryContents(
owner: string,
repo: string,
path: string,
ref?: string,
): Promise<
Array<{
download_url: string;
name: 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;
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;
}
/**
* 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;
}
// ステラリスの現在のバージョン情報を取得するツール
server.addTool({
annotations: {
openWorldHint: true, // 外部APIにアクセスする
readOnlyHint: true, // データを変更しない
title: "Stellaris Version Info",
},
description:
"ステラリスの現在のバージョン情報を取得します。ビルドID、最終更新日時、利用可能なバージョン番号などmodding に必要な情報を提供します。",
execute: async (_args, { log }) => {
try {
log.info("Fetching Stellaris version info from SteamCMD API...");
const appInfo = (await fetchSteamAppInfo(STELLARIS_APP_ID)) as {
common?: { name?: string; oslist?: string };
config?: { installdir?: string };
depots?: {
branches?: Record<
string,
{ buildid?: string; description?: string; timeupdated?: string }
>;
};
extended?: { developer?: string; publisher?: string };
};
const branches = appInfo.depots?.branches;
if (!branches) {
throw new Error("Branch information not found in API response");
}
const publicBranch = branches.public;
// バージョン番号らしきブランチを抽出(数字.数字.数字の形式)
const versionBranches = Object.keys(branches)
.filter((branch) => /^\d+\.\d+/.test(branch))
.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;
});
const latestVersionBranch = versionBranches[0];
const currentVersionInfo = latestVersionBranch
? branches[latestVersionBranch]
: null;
const result = {
availableVersions: versionBranches.slice(0, 10), // 最新10バージョンを表示
currentPublic: {
buildId: publicBranch?.buildid,
lastUpdated: publicBranch?.timeupdated
? formatTimestamp(publicBranch.timeupdated)
: "Unknown",
lastUpdatedUnix: publicBranch?.timeupdated,
},
gameInfo: {
appId: STELLARIS_APP_ID,
developer: appInfo.extended?.developer,
installDir: appInfo.config?.installdir,
publisher: appInfo.extended?.publisher,
supportedOS: appInfo.common?.oslist,
},
gameName: appInfo.common?.name || "Stellaris",
latestVersion: latestVersionBranch
? {
buildId: currentVersionInfo?.buildid,
description: currentVersionInfo?.description || "説明なし",
lastUpdated: currentVersionInfo?.timeupdated
? formatTimestamp(currentVersionInfo.timeupdated)
: "Unknown",
lastUpdatedUnix: currentVersionInfo?.timeupdated,
version: latestVersionBranch,
}
: null,
specialBranches: Object.keys(branches).filter(
(branch) => !versionBranches.includes(branch) && branch !== "public",
),
};
log.info("Successfully retrieved Stellaris version info", {
latestVersion: result.latestVersion?.version,
publicBuildId: result.currentPublic.buildId,
});
return {
content: [
{
text: `# ステラリス バージョン情報
## 現在のPublicブランチ
- **ビルドID**: ${result.currentPublic.buildId}
- **最終更新**: ${result.currentPublic.lastUpdated}
${result.latestVersion
? `## 最新バージョン
- **バージョン**: ${result.latestVersion.version}
- **ビルドID**: ${result.latestVersion.buildId}
- **説明**: ${result.latestVersion.description}
- **最終更新**: ${result.latestVersion.lastUpdated}`
: ""
}
## ゲーム情報
- **ゲーム名**: ${result.gameName}
- **App ID**: ${result.gameInfo.appId}
- **インストールディレクトリ**: ${result.gameInfo.installDir}
- **対応OS**: ${result.gameInfo.supportedOS}
- **開発者**: ${result.gameInfo.developer}
- **パブリッシャー**: ${result.gameInfo.publisher}
## 利用可能なバージョン(最新10件)
${result.availableVersions.map((version) => `- ${version}`).join("\n")}
## 特別なブランチ
${result.specialBranches.map((branch) => `- ${branch}`).join("\n")}
---
*データ取得元: SteamCMD API (https://api.steamcmd.net/)*`,
type: "text",
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error("Failed to fetch Stellaris version info", {
error: errorMessage,
});
throw new Error(
`ステラリスのバージョン情報の取得に失敗しました: ${errorMessage}`,
);
}
},
name: "stellaris-version",
parameters: z.object({}), // パラメータなし
});
// CWToolsのStellarisコンフィグルールを取得するツール
server.addTool({
annotations: {
openWorldHint: true, // GitHub APIにアクセスする
readOnlyHint: true, // データを変更しない
title: "CWTools Stellaris Config",
},
description:
"CWToolsのStellarisコンフィグルール(.cwt files)を取得します。最新の安定版または開発版から、config/直下およびサブディレクトリ(common/等)内の特定のファイルまたは全ファイルを取得できます。",
execute: async (args, { log }) => {
try {
log.info("Fetching CWTools Stellaris config...", {
file: args.file,
listOnly: args.listOnly,
version: args.version,
});
// 参照するブランチ/タグを決定
let ref: string | undefined;
if (args.version === "stable") {
ref = await getLatestTag(CWTOOLS_REPO_OWNER, CWTOOLS_REPO_NAME);
log.info(`Using stable version: ${ref}`);
} else {
log.info("Using latest development version (main branch)");
}
// configディレクトリの内容を取得
const configContents = await getDirectoryContents(
CWTOOLS_REPO_OWNER,
CWTOOLS_REPO_NAME,
"config",
ref,
);
// .cwtファイルのみをフィルタ(ルートレベル)
const rootCwtFiles = configContents.filter(
(item) => item.type === "file" && item.name.endsWith(".cwt"),
);
// サブディレクトリからも.cwtファイルを取得
const subdirectories = configContents.filter(
(item) => item.type === "dir",
);
const nestedCwtFiles: Array<{
download_url: string;
name: string;
path: string;
size: number;
}> = [];
for (const subdir of subdirectories) {
let retryCount = 0;
const maxSubdirRetries = 2;
while (retryCount <= maxSubdirRetries) {
try {
log.debug(
`Reading subdirectory: ${subdir.name} (attempt ${retryCount + 1})`,
);
const subdirContents = await getDirectoryContents(
CWTOOLS_REPO_OWNER,
CWTOOLS_REPO_NAME,
`config/${subdir.name}`,
ref,
);
const subdirCwtFiles = subdirContents
.filter(
(item) => item.type === "file" && item.name.endsWith(".cwt"),
)
.map((file) => ({
download_url: file.download_url,
name: file.name,
path: `${subdir.name}/${file.name}`,
size: file.size,
}));
nestedCwtFiles.push(...subdirCwtFiles);
log.info(
`Successfully read ${subdirCwtFiles.length} .cwt files from ${subdir.name}/`,
);
// 成功したらループを抜ける
break;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (retryCount < maxSubdirRetries) {
const delay = 1000 * (retryCount + 1); // 1秒、2秒、3秒
log.warn(
`Failed to read subdirectory ${subdir.name} (attempt ${retryCount + 1}), retrying in ${delay}ms`,
{
error: errorMessage,
},
);
await new Promise((resolve) => setTimeout(resolve, delay));
retryCount++;
} else {
log.error(
`Failed to read subdirectory ${subdir.name} after ${maxSubdirRetries + 1} attempts`,
{
error: errorMessage,
},
);
break;
}
}
}
// API制限を考慮して少し待機
await new Promise((resolve) => setTimeout(resolve, 200));
}
// 全ての.cwtファイルを統合
const allCwtFiles = [
...rootCwtFiles.map((file) => ({
download_url: file.download_url,
name: file.name,
path: file.name,
size: file.size,
})),
...nestedCwtFiles,
];
// 後続の処理で使用するため、cwtFilesをallCwtFilesに置き換え
const cwtFiles = allCwtFiles;
if (args.listOnly) {
// ファイル一覧のみを返す
const fileList = cwtFiles.map((file) => ({
downloadUrl: file.download_url,
name: file.name,
path: file.path,
size: file.size,
}));
return {
content: [
{
text: `# CWTools Stellaris Config Files (${args.version})
${ref ? `**Version**: ${ref}` : "**Version**: Latest development"}
**Repository**: ${CWTOOLS_REPO_OWNER}/${CWTOOLS_REPO_NAME}
**Total .cwt files**: ${fileList.length}
## Available Config Files
${fileList.map((file) => `- **${file.path || file.name}** (${file.size} bytes)`).join("\n")}
---
*Use the tool again with a specific file parameter to fetch the content of any file.*`,
type: "text",
},
],
};
}
if (args.file) {
// 特定のファイルを取得(ファイル名またはパスで検索)
const targetFile = cwtFiles.find(
(file) =>
file.name === args.file ||
file.path === args.file ||
`${file.path}` === args.file,
);
if (!targetFile) {
throw new Error(
`File "${args.file}" not found. Available files: ${cwtFiles.map((f) => f.path || f.name).join(", ")}`,
);
}
const fileContent = await getFileContent(
CWTOOLS_REPO_OWNER,
CWTOOLS_REPO_NAME,
`config/${targetFile.path}`,
ref,
);
return {
content: [
{
text: `# ${targetFile.path || args.file}
${ref ? `**Version**: ${ref}` : "**Version**: Latest development"}
**Repository**: ${CWTOOLS_REPO_OWNER}/${CWTOOLS_REPO_NAME}
**File Size**: ${targetFile.size} bytes
## Content
\`\`\`cwt
${fileContent}
\`\`\`
---
*This is a CWTools config file (.cwt) that defines validation rules for Stellaris modding.*`,
type: "text",
},
],
};
}
// 全ファイルを取得(最大10ファイルまで)
const filesToFetch = cwtFiles.slice(0, 10);
const fileContents: Array<{
content: string;
name: string;
size: number;
}> = [];
for (const file of filesToFetch) {
let retryCount = 0;
const maxFileRetries = 2;
while (retryCount <= maxFileRetries) {
try {
log.debug(
`Fetching file: ${file.path || file.name} (attempt ${retryCount + 1})`,
);
const content = await getFileContent(
CWTOOLS_REPO_OWNER,
CWTOOLS_REPO_NAME,
`config/${file.path}`,
ref,
);
fileContents.push({
content,
name: file.path || file.name,
size: file.size,
});
log.debug(
`Successfully fetched ${file.path || file.name} (${content.length} characters)`,
);
break;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (retryCount < maxFileRetries) {
const delay = 1000 * (retryCount + 1);
log.warn(
`Failed to fetch ${file.path || file.name} (attempt ${retryCount + 1}), retrying in ${delay}ms`,
{
error: errorMessage,
},
);
await new Promise((resolve) => setTimeout(resolve, delay));
retryCount++;
} else {
log.error(
`Failed to fetch ${file.path || file.name} after ${maxFileRetries + 1} attempts`,
{
error: errorMessage,
},
);
break;
}
}
}
// API制限を考慮して少し待機
await new Promise((resolve) => setTimeout(resolve, 150));
}
const totalFiles = cwtFiles.length;
const fetchedFiles = fileContents.length;
return {
content: [
{
text: `# CWTools Stellaris Config Files (${args.version})
${ref ? `**Version**: ${ref}` : "**Version**: Latest development"}
**Repository**: ${CWTOOLS_REPO_OWNER}/${CWTOOLS_REPO_NAME}
**Total files**: ${totalFiles} (showing first ${fetchedFiles})
${fileContents
.map(
(file) => `## ${file.name} (${file.size} bytes)
\`\`\`cwt
${file.content}
\`\`\`
---`,
)
.join("\n\n")}
${totalFiles > 10 ? `\n*Note: Only showing first 10 files. Use the 'file' parameter to fetch specific files, or 'listOnly' to see all available files.*` : ""}
---
*These are CWTools config files (.cwt) that define validation rules for Stellaris modding.*`,
type: "text",
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error("Failed to fetch CWTools config", { error: errorMessage });
throw new Error(`CWToolsコンフィグの取得に失敗しました: ${errorMessage}`);
}
},
name: "cwtools-config",
parameters: z.object({
file: z
.string()
.optional()
.describe(
"取得する特定の.cwtファイル名またはパス (例: common.cwt, common/agreements.cwt)",
),
listOnly: z
.boolean()
.default(false)
.describe("ファイル一覧のみを表示する場合はtrue"),
version: z
.enum(["stable", "latest"])
.default("stable")
.describe("取得するバージョン: stable (最新タグ) または latest (開発版)"),
}),
});
// Stellarisドキュメントからキーワード検索するツール
server.addTool({
annotations: {
openWorldHint: true, // GitHub APIにアクセスする
readOnlyHint: true, // データを変更しない
title: "Stellaris Documentation Search",
},
description:
"OldEnt/stellaris-triggers-modifiers-effects-listリポジトリからStellarisの最新バージョンのドキュメントを検索し、指定したキーワードに一致する箇所をコードスニペットとして返します。大きなファイルを効率的に検索できます。",
execute: async (args, { log }) => {
try {
log.info("Searching Stellaris documentation...", {
docType: args.docType,
keyword: args.keyword,
});
// 利用可能なバージョン一覧を取得
const availableVersions = await getAvailableStellarisVersions(
STELLARIS_DOCS_OWNER,
STELLARIS_DOCS_REPO,
);
if (availableVersions.length === 0) {
throw new Error(
"No Stellaris documentation versions found in repository",
);
}
// 最新バージョンから順番に試行
let targetVersion: null | string = null;
let fileContent: null | string = null;
let fileName: null | string = null;
const attemptedVersions: string[] = [];
for (const version of availableVersions) {
try {
fileName = `${version}_game_${args.docType}.log`;
log.info(`Attempting to fetch: ${fileName}`);
fileContent = await getFileContent(
STELLARIS_DOCS_OWNER,
STELLARIS_DOCS_REPO,
fileName,
);
targetVersion = version;
log.info(`Successfully found documentation for version: ${version}`);
break;
} catch {
attemptedVersions.push(version);
log.warn(
`Version ${version} does not have ${args.docType} documentation, trying next version...`,
);
continue;
}
}
if (!targetVersion || !fileContent || !fileName) {
throw new Error(
`No ${args.docType} documentation found for any available version. Attempted versions: ${attemptedVersions.join(", ")}`,
);
}
// キーワード検索を実行
const lines = fileContent.split("\n");
const searchResults: Array<{
lineNumber: number;
content: string;
context: string[];
}> = [];
const keyword = args.keyword.toLowerCase();
const contextLines = args.contextLines || 2;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.toLowerCase().includes(keyword)) {
// 前後のコンテキストを取得
const startIndex = Math.max(0, i - contextLines);
const endIndex = Math.min(lines.length - 1, i + contextLines);
const context = lines.slice(startIndex, endIndex + 1);
searchResults.push({
lineNumber: i + 1,
content: line.trim(),
context,
});
}
}
const versionLabel =
targetVersion === availableVersions[0]
? `${targetVersion} (Latest)`
: `${targetVersion} (Fallback - Latest available for ${args.docType})`;
if (searchResults.length === 0) {
return {
content: [
{
text: `# Stellaris ${args.docType.toUpperCase()} Search Results
**Version**: ${versionLabel}
**Repository**: ${STELLARIS_DOCS_OWNER}/${STELLARIS_DOCS_REPO}
**File**: ${fileName}
**Keyword**: "${args.keyword}"
**Results**: No matches found
キーワード "${args.keyword}" に一致する項目が見つかりませんでした。
**検索のヒント:**
- 大文字小文字は区別されません
- 部分一致で検索されます
- 英語のキーワードを使用してください
---
*This documentation is automatically extracted from the Stellaris game engine and compiled by OldEnt.*`,
type: "text",
},
],
};
}
// 結果を制限(最大20件)
const limitedResults = searchResults.slice(0, 20);
const totalResults = searchResults.length;
return {
content: [
{
text: `# Stellaris ${args.docType.toUpperCase()} Search Results
**Version**: ${versionLabel}
**Repository**: ${STELLARIS_DOCS_OWNER}/${STELLARIS_DOCS_REPO}
**File**: ${fileName}
**Keyword**: "${args.keyword}"
**Results**: ${totalResults} matches found${totalResults > 20 ? " (showing first 20)" : ""}
## Search Results
${limitedResults
.map(
(result, index) => `### Result ${index + 1} (Line ${result.lineNumber})
**Matching Line**: \`${result.content}\`
**Context**:
\`\`\`
${result.context.map((line, i) => {
const lineNum = result.lineNumber - contextLines + i;
const isMatch = i === contextLines;
return `${lineNum.toString().padStart(4, " ")}${isMatch ? ">" : ":"} ${line}`;
}).join("\n")}
\`\`\`
---`,
)
.join("\n\n")}
${totalResults > 20 ? `\n*Note: Showing first 20 results out of ${totalResults} total matches. Refine your search keyword for more specific results.*` : ""}
---
*This documentation is automatically extracted from the Stellaris game engine and compiled by OldEnt with the help of McAwesome, Erdnuss and the rest of the Stellaris Modding Den Discord crew.*`,
type: "text",
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error("Failed to search Stellaris documentation", {
error: errorMessage,
});
throw new Error(
`Stellarisドキュメントの検索に失敗しました: ${errorMessage}`,
);
}
},
name: "stellaris-search",
parameters: z.object({
docType: z
.enum(["triggers", "effects", "modifiers", "scopes", "localizations"])
.describe(
"検索するドキュメントの種類: triggers (条件), effects (効果), modifiers (修正値), scopes (スコープ), localizations (ローカライゼーション)",
),
keyword: z
.string()
.min(1)
.describe("検索するキーワード(英語推奨、大文字小文字区別なし)"),
contextLines: z
.number()
.min(0)
.max(10)
.default(2)
.describe("一致した行の前後に表示するコンテキスト行数(デフォルト: 2)"),
}),
});
// Stellarisの要素が最新バージョンで有効かどうかをチェックするツール
server.addTool({
annotations: {
openWorldHint: true, // GitHub APIにアクセスする
readOnlyHint: true, // データを変更しない
title: "Stellaris Element Validator",
},
description:
"指定したtrigger、effect、modifier、scope、localizationが最新バージョンのStellarisで有効かどうかをチェックします。modding時の互換性確認に便利です。",
execute: async (args, { log }) => {
try {
log.info("Validating Stellaris element...", {
docType: args.docType,
element: args.element,
});
// 利用可能なバージョン一覧を取得
const availableVersions = await getAvailableStellarisVersions(
STELLARIS_DOCS_OWNER,
STELLARIS_DOCS_REPO,
);
if (availableVersions.length === 0) {
throw new Error(
"No Stellaris documentation versions found in repository",
);
}
// 最新バージョンから順番に試行
let targetVersion: null | string = null;
let fileContent: null | string = null;
let fileName: null | string = null;
const attemptedVersions: string[] = [];
for (const version of availableVersions) {
try {
fileName = `${version}_game_${args.docType}.log`;
log.info(`Attempting to fetch: ${fileName}`);
fileContent = await getFileContent(
STELLARIS_DOCS_OWNER,
STELLARIS_DOCS_REPO,
fileName,
);
targetVersion = version;
log.info(`Successfully found documentation for version: ${version}`);
break;
} catch {
attemptedVersions.push(version);
log.warn(
`Version ${version} does not have ${args.docType} documentation, trying next version...`,
);
continue;
}
}
if (!targetVersion || !fileContent || !fileName) {
throw new Error(
`No ${args.docType} documentation found for any available version. Attempted versions: ${attemptedVersions.join(", ")}`,
);
}
// 要素の存在をチェック
const lines = fileContent.split("\n");
const element = args.element.toLowerCase();
const exactMatches: Array<{
lineNumber: number;
content: string;
}> = [];
const partialMatches: Array<{
lineNumber: number;
content: string;
}> = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineLower = line.toLowerCase();
// 完全一致(単語境界を考慮)
const exactMatch = new RegExp(`\\b${element}\\b`).test(lineLower);
if (exactMatch) {
exactMatches.push({
lineNumber: i + 1,
content: line,
});
}
// 部分一致
else if (lineLower.includes(element)) {
partialMatches.push({
lineNumber: i + 1,
content: line,
});
}
}
const versionLabel =
targetVersion === availableVersions[0]
? `${targetVersion} (Latest)`
: `${targetVersion} (Fallback - Latest available for ${args.docType})`;
const isValid = exactMatches.length > 0;
const hasPartialMatches = partialMatches.length > 0;
return {
content: [
{
text: `# Stellaris ${args.docType.toUpperCase()} Validation
**Version**: ${versionLabel}
**Repository**: ${STELLARIS_DOCS_OWNER}/${STELLARIS_DOCS_REPO}
**File**: ${fileName}
**Element**: "${args.element}"
**Status**: ${isValid ? "✅ **VALID**" : hasPartialMatches ? "⚠️ **PARTIAL MATCH**" : "❌ **NOT FOUND**"}
## Validation Results
${
isValid
? `### ✅ Exact Matches Found (${exactMatches.length})
The element "${args.element}" is **valid** in the latest Stellaris version.
**Occurrences:**
${exactMatches
.slice(0, 10)
.map(
(match) => `- Line ${match.lineNumber}: \`${match.content}\``,
)
.join("\n")}
${exactMatches.length > 10 ? `\n*... and ${exactMatches.length - 10} more occurrences*` : ""}`
: hasPartialMatches
? `### ⚠️ Partial Matches Found (${partialMatches.length})
The element "${args.element}" was not found as an exact match, but similar elements were found:
**Similar Elements:**
${partialMatches
.slice(0, 10)
.map(
(match) => `- Line ${match.lineNumber}: \`${match.content}\``,
)
.join("\n")}
${partialMatches.length > 10 ? `\n*... and ${partialMatches.length - 10} more similar elements*` : ""}
**Suggestion**: Check if you meant one of the similar elements above, or verify the exact spelling.`
: `### ❌ Element Not Found
The element "${args.element}" was not found in the ${args.docType} documentation.
**Possible reasons:**
- The element name is misspelled
- The element has been removed or renamed in recent versions
- The element belongs to a different category (try searching in other docTypes)
**Suggestion**: Try using the \`stellaris-search\` tool to search for similar elements.`
}
## Usage Information
${
isValid
? `This ${args.docType.slice(0, -1)} can be safely used in your Stellaris mods for version ${targetVersion} and should be compatible with the current game version.`
: hasPartialMatches
? `While "${args.element}" wasn't found exactly, there are similar elements available. Please verify the correct spelling or consider using one of the similar elements shown above.`
: `This element is not available in the current Stellaris version. Consider using alternative elements or check if the functionality has been moved to a different category.`
}
---
*This validation is based on documentation automatically extracted from the Stellaris game engine and compiled by OldEnt.*`,
type: "text",
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
log.error("Failed to validate Stellaris element", {
error: errorMessage,
});
throw new Error(
`Stellaris要素の検証に失敗しました: ${errorMessage}`,
);
}
},
name: "stellaris-validate",
parameters: z.object({
docType: z
.enum(["triggers", "effects", "modifiers", "scopes", "localizations"])
.describe(
"検証するドキュメントの種類: triggers (条件), effects (効果), modifiers (修正値), scopes (スコープ), localizations (ローカライゼーション)",
),
element: z
.string()
.min(1)
.describe("検証する要素名(例: has_technology, add_modifier, naval_capacity)"),
}),
});
server.start({
transportType: "stdio",
});