endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
104 lines (103 loc) • 4.62 kB
TypeScript
/**
* GitHub-backed data sync for Endfield-MCP.
*
* Mirrors the design of PRTS-MCP's `ts/src/data/sync.ts` — the same proven
* decision tree, cascade fallback, and atomic-write semantics. The only
* Endfield-specific parts are the User-Agent and the absence of the
* `upstream-<sha>` tag convention (our mirror uses plain semver tags, so
* commitSha === tag verbatim).
*
* Contract:
* - Downloads GitHub Release zip assets only when the release tag changes.
* - Skips the upstream API call entirely when cached data is fresher than
* CACHE_TTL_SECONDS (avoids burning GitHub anonymous quota on every start).
* - Falls back gracefully to cached data when the network is unavailable,
* and to "no data" only when neither cache nor network can provide.
* - All writes are write-to-tmp-then-rename for crash safety.
*/
/** Describes an upstream GitHub repository and the files required from it. */
export interface RepoSpec {
owner: string;
repo: string;
branch: string;
files: readonly string[];
/** Absolute path to the local directory where files are written. */
localRoot: string;
}
export type SyncStatus = "updated" | "up_to_date" | "offline_fallback" | "no_data";
export interface SyncResult {
spec: RepoSpec;
status: SyncStatus;
commitSha: string | null;
error: string | null;
}
/** Describes a GitHub Release asset to download as a local zip. */
export interface ReleaseSpec {
owner: string;
repo: string;
/** Asset filename in the release, e.g. "endfield-tables.zip". */
assetName: string;
/** Absolute destination path for the downloaded zip. */
localZip: string;
/** Optional validator returning missing or invalid zip entries. */
validateZip?: (zipPath: string) => string[];
}
/** Describes a GitHub Release zip asset that should be extracted locally. */
export interface ReleaseArchiveSpec {
owner: string;
repo: string;
assetName: string;
localZip: string;
localRoot: string;
requiredFiles: readonly string[];
}
/**
* Fetch the release tag and asset download URL for the requested asset.
*
* Lists the repo's releases (newest first) and returns the first Release
* that actually contains `spec.assetName`. We deliberately do NOT use the
* `/releases/latest` endpoint: that returns the single newest Release for the
* whole repo, which only carries *that* release's assets. Once a repo ships
* several independent assets across different Releases (tables/story/worldview),
* `/releases/latest` makes every non-latest asset look "missing" and breaks
* their syncs. Listing + asset-name matching is robust to that layout.
*
* Return / throw contract (load-bearing — `syncRelease` distinguishes the two):
* - Returns `{tag, url}` when a Release carrying the asset is found.
* - Returns `null` when the API responded but NO scanned Release has the
* asset (genuine absence — caller must NOT retry via blind download).
* - THROWS on network/API failure (caller treats as "unreachable" and may
* fall back to blind download / cached data).
*
* This distinction matters because the blind-download fallback in `syncRelease`
* constructs a `releases/latest/download/<asset>` URL — which only makes sense
* when we genuinely couldn't reach the API, not when the API confirmed the
* asset is absent.
*/
export declare function checkLatestRelease(spec: ReleaseSpec, timeoutMs?: number): Promise<{
tag: string;
url: string;
} | null>;
/**
* Download the release asset zip atomically, then write cache metadata.
* Uses write-to-tmp-then-rename for crash safety.
*/
export declare function downloadReleaseAsset(spec: ReleaseSpec, tag: string, assetUrl: string, timeoutMs?: number): Promise<void>;
/**
* Check latest GitHub Release and download the zip if the tag has changed.
*
* Decision tree:
* 1. Cache fresh AND zip exists → up_to_date (skip API call).
* 2. Network failure → offline_fallback (zip exists) / no_data (no zip).
* 3. Tag unchanged AND zip exists → up_to_date (refresh fetchedAt).
* 4. Tag changed or zip missing → downloadReleaseAsset → updated / fallback.
*/
export declare function syncRelease(spec: ReleaseSpec): Promise<SyncResult>;
/**
* Download a GitHub Release zip asset and extract it into localRoot.
*
* Keeps the data distribution path aligned with PRTS-MCP's pattern
* (release zip → local extraction → DirectoryStore reads) while preserving
* whatever on-disk layout the Endfield mirror ships.
*/
export declare function syncReleaseArchive(spec: ReleaseArchiveSpec): Promise<SyncResult>;