gameye-mcp 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -27
- package/dist/dockerfile-check.d.ts +84 -0
- package/dist/dockerfile-check.js +278 -0
- package/dist/env.d.ts +23 -2
- package/dist/env.js +61 -9
- package/dist/generated/skills.d.ts +39 -0
- package/dist/generated/skills.js +220 -0
- package/dist/index.js +9 -3
- package/dist/limits.d.ts +40 -0
- package/dist/limits.js +131 -0
- package/dist/server.d.ts +16 -2
- package/dist/server.js +34 -3
- package/dist/tokenless.d.ts +36 -0
- package/dist/tokenless.js +97 -0
- package/dist/tools/bundled-skills.d.ts +7 -0
- package/dist/tools/bundled-skills.js +76 -0
- package/dist/tools/common.d.ts +14 -2
- package/dist/tools/common.js +9 -2
- package/dist/tools/context.js +15 -1
- package/dist/tools/dockerfile-check.d.ts +3 -0
- package/dist/tools/dockerfile-check.js +27 -0
- package/dist/tools/register.js +6 -0
- package/dist/tools/sessions.js +31 -7
- package/dist/tools/skills.js +8 -2
- package/dist/tools/templates.d.ts +3 -0
- package/dist/tools/templates.js +40 -0
- package/dist/types.d.ts +1 -0
- package/package.json +3 -1
package/dist/tools/register.js
CHANGED
|
@@ -9,6 +9,9 @@ import { registerTokenTools } from "./tokens.js";
|
|
|
9
9
|
import { registerLogTools } from "./logs.js";
|
|
10
10
|
import { registerArtifactTools } from "./artifacts.js";
|
|
11
11
|
import { registerSkillsTools } from "./skills.js";
|
|
12
|
+
import { registerBundledSkillsTools } from "./bundled-skills.js";
|
|
13
|
+
import { registerDockerfileCheckTool } from "./dockerfile-check.js";
|
|
14
|
+
import { registerTemplateTools } from "./templates.js";
|
|
12
15
|
/** Register every tool. */
|
|
13
16
|
export function registerAllTools(server, client, ctx) {
|
|
14
17
|
registerContextTool(server, ctx);
|
|
@@ -22,4 +25,7 @@ export function registerAllTools(server, client, ctx) {
|
|
|
22
25
|
registerArtifactTools(server, client, ctx);
|
|
23
26
|
registerTokenTools(server, client, ctx);
|
|
24
27
|
registerSkillsTools(server, ctx);
|
|
28
|
+
registerBundledSkillsTools(server, ctx);
|
|
29
|
+
registerTemplateTools(server, ctx);
|
|
30
|
+
registerDockerfileCheckTool(server, ctx);
|
|
25
31
|
}
|
package/dist/tools/sessions.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { GameyeClient } from "../client.js";
|
|
3
|
-
import { AGENT_LABEL_KEY, AGENT_LABEL_VALUE, DEFAULT_SESSION_TTL, applyLaunchDefaults, assertExplicitSessionId, assertNoTokenInLaunch, describeTtl, formatDryRun, } from "../guards.js";
|
|
2
|
+
import { GameyeClient, GameyeError } from "../client.js";
|
|
3
|
+
import { AGENT_LABEL_KEY, AGENT_LABEL_VALUE, DEFAULT_SESSION_TTL, applyLaunchDefaults, assertExplicitSessionId, assertNoTokenInLaunch, describeTtl, formatDryRun, GuardError, } from "../guards.js";
|
|
4
|
+
import { SessionLimiter } from "../limits.js";
|
|
4
5
|
import { defineTool, dryRunInput, formatLabels } from "./common.js";
|
|
5
6
|
function formatPorts(ports) {
|
|
6
7
|
return (ports ?? []).map((p) => `${p.host}:${p.container}/${p.type}`).join(", ");
|
|
7
8
|
}
|
|
8
9
|
export function registerSessionTools(server, client, ctx) {
|
|
10
|
+
const limiter = new SessionLimiter(ctx.limits, client);
|
|
11
|
+
const { maxSessions, maxTtl, maxLaunches, stopOwnOnly } = ctx.limits;
|
|
9
12
|
// ─── Launch Session ──────────────────────────────────────────────────────────
|
|
10
13
|
defineTool(server, ctx, "gameye_launch_session", {
|
|
11
14
|
title: "Launch session",
|
|
12
15
|
description: "Start a containerized game server session on the Gameye platform. Returns the session ID, host and ports. Starts a billable session in the environment shown in the result prefix. " +
|
|
13
16
|
`When no ttl is given, a ${DEFAULT_SESSION_TTL} TTL is applied and reported. The label ${AGENT_LABEL_KEY}=${AGENT_LABEL_VALUE} is always sent and replaces any ${AGENT_LABEL_KEY} value you pass; ` +
|
|
14
17
|
"if a warm pool serves the image and region, the server ignores request labels (and env and args), so warm-pool sessions do not carry it. " +
|
|
15
|
-
"The launch is refused, with nothing sent, if any env value, arg or label contains this server's Gameye API token.
|
|
18
|
+
"The launch is refused, with nothing sent, if any env value, arg or label contains this server's Gameye API token. " +
|
|
19
|
+
`It is also refused if the ttl is above ${maxTtl}, if ${maxSessions} sessions started by this server are already running, or after ${maxLaunches} launches by this server process; ` +
|
|
20
|
+
"these limits are set by the user when the server starts, and a refusal says which one applied. Pass dryRun=true to preview the request.",
|
|
16
21
|
inputSchema: {
|
|
17
22
|
location: z.string().describe("Region/datacenter to host the session (e.g. 'europe', 'us-east')"),
|
|
18
23
|
image: z.string().describe("Docker image name to run (e.g. 'csgo', 'tf2')"),
|
|
@@ -25,18 +30,30 @@ export function registerSessionTools(server, client, ctx) {
|
|
|
25
30
|
ttl: z
|
|
26
31
|
.string()
|
|
27
32
|
.optional()
|
|
28
|
-
.describe(`Session time-to-live, e.g. '1h', '30m'. Session auto-stops after this duration. Defaults to ${DEFAULT_SESSION_TTL}`),
|
|
33
|
+
.describe(`Session time-to-live, e.g. '1h', '30m'. Session auto-stops after this duration. Defaults to ${DEFAULT_SESSION_TTL}; at most ${maxTtl}`),
|
|
29
34
|
dryRun: dryRunInput,
|
|
30
35
|
},
|
|
31
36
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
32
37
|
}, async ({ dryRun, ...input }) => {
|
|
33
38
|
assertNoTokenInLaunch(input, client);
|
|
34
39
|
const { request, defaultTtlApplied } = applyLaunchDefaults(input);
|
|
35
|
-
const
|
|
40
|
+
const ttl = request.ttl ?? DEFAULT_SESSION_TTL;
|
|
41
|
+
const ttlMs = limiter.checkTtl(ttl);
|
|
42
|
+
const ttlLine = ` TTL: ${describeTtl(ttl, defaultTtlApplied)}`;
|
|
36
43
|
if (dryRun) {
|
|
37
44
|
return `${formatDryRun(ctx, GameyeClient.launchSessionRequest(request), client)}\n\n${ttlLine.trim()}`;
|
|
38
45
|
}
|
|
39
|
-
const
|
|
46
|
+
const slot = await limiter.reserve(ttlMs);
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = await client.launchSession(request);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// A 4xx means nothing started. A 5xx, timeout or network error may have started one.
|
|
53
|
+
slot.failed(!(err instanceof GameyeError && err.statusCode < 500));
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
slot.started(result.id);
|
|
40
57
|
const lines = [`Session launched successfully.`, ` Session ID: ${result.id}`, ttlLine];
|
|
41
58
|
if (result.host)
|
|
42
59
|
lines.push(` Host: ${result.host}`);
|
|
@@ -123,7 +140,9 @@ export function registerSessionTools(server, client, ctx) {
|
|
|
123
140
|
defineTool(server, ctx, "gameye_stop_session", {
|
|
124
141
|
title: "Stop session",
|
|
125
142
|
description: "Terminate one Gameye session by its exact ID (sends SIGTERM to the container). Any players connected to it are disconnected. The session transitions to 'draining' then 'shuttingdown' then 'exited'. " +
|
|
126
|
-
"There is no bulk or implicit stop: an empty, wildcard or list ID is refused.
|
|
143
|
+
"There is no bulk or implicit stop: an empty, wildcard or list ID is refused. " +
|
|
144
|
+
(stopOwnOnly ? "This server only stops sessions it launched itself (GAMEYE_STOP_OWN_ONLY=1). " : "") +
|
|
145
|
+
"Pass dryRun=true to preview the request.",
|
|
127
146
|
inputSchema: {
|
|
128
147
|
id: z.string().min(1).describe("Exact ID of the one session to stop"),
|
|
129
148
|
dryRun: dryRunInput,
|
|
@@ -131,9 +150,14 @@ export function registerSessionTools(server, client, ctx) {
|
|
|
131
150
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
132
151
|
}, async ({ id: rawId, dryRun }) => {
|
|
133
152
|
const id = assertExplicitSessionId(rawId);
|
|
153
|
+
if (stopOwnOnly && !limiter.isOwn(id)) {
|
|
154
|
+
throw new GuardError(`Stop refused: session ${id} was not launched by this server, and it only stops its own sessions (GAMEYE_STOP_OWN_ONLY=1). ` +
|
|
155
|
+
"Ask the user to stop it in the Admin Panel or from another tool. No request was sent.");
|
|
156
|
+
}
|
|
134
157
|
if (dryRun)
|
|
135
158
|
return formatDryRun(ctx, GameyeClient.stopSessionRequest(id), client);
|
|
136
159
|
await client.stopSession(id);
|
|
160
|
+
limiter.stopped(id);
|
|
137
161
|
return `Session ${id} stop signal sent. The session is now draining.\n\nUse gameye_describe_session to monitor shutdown progress.`;
|
|
138
162
|
});
|
|
139
163
|
}
|
package/dist/tools/skills.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineTool } from "./common.js";
|
|
3
3
|
import { compareVersions, defaultSkillsEnv, describeInstall, ENABLE_UPDATE_ENV, fetchLatestPackVersion, findSkillsInstalls, isOutdated, updateCommands, } from "../skills.js";
|
|
4
|
+
import { BUNDLED_SKILLS_PACK_VERSION } from "../generated/skills.js";
|
|
4
5
|
const RELOAD_NOTE = "The running session keeps the skills it already loaded. Start a new session (in Claude Code, /reload-plugins also works for the plugin) to use the update.";
|
|
5
6
|
export function registerSkillsTools(server, ctx, env = defaultSkillsEnv()) {
|
|
6
7
|
defineTool(server, ctx, "gameye_update_skills", {
|
|
@@ -10,7 +11,9 @@ export function registerSkillsTools(server, ctx, env = defaultSkillsEnv()) {
|
|
|
10
11
|
"Without apply it only checks and changes nothing. With apply=true it runs each outdated install's own updater " +
|
|
11
12
|
"(npx skills update for the Gameye skills only, or claude plugin update) when the operator set " +
|
|
12
13
|
`${ENABLE_UPDATE_ENV}=1; otherwise it lists the commands for the user to run. Ask the user before applying. ` +
|
|
13
|
-
|
|
14
|
+
`Also reports the skills pack bundled with this server (${BUNDLED_SKILLS_PACK_VERSION}). ` +
|
|
15
|
+
"Updates come only through those package managers. This server ships a pinned copy of the skills reviewed at build time; " +
|
|
16
|
+
"the skill and template tools never fetch skill text at runtime, and only gameye_update_skills reaches GitHub, when called.",
|
|
14
17
|
inputSchema: {
|
|
15
18
|
apply: z
|
|
16
19
|
.boolean()
|
|
@@ -23,8 +26,10 @@ export function registerSkillsTools(server, ctx, env = defaultSkillsEnv()) {
|
|
|
23
26
|
.describe("The `Pack version:` from the gameye skill you have loaded, used when no install is found on disk"),
|
|
24
27
|
},
|
|
25
28
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
29
|
+
requiresToken: false,
|
|
26
30
|
}, async ({ apply, installedVersion }) => {
|
|
27
31
|
const installs = await findSkillsInstalls(env);
|
|
32
|
+
const bundled = `Bundled with this server: pack ${BUNDLED_SKILLS_PACK_VERSION} (read it with gameye_list_skills and gameye_get_skill)`;
|
|
28
33
|
let latest;
|
|
29
34
|
try {
|
|
30
35
|
latest = await fetchLatestPackVersion(env);
|
|
@@ -33,12 +38,13 @@ export function registerSkillsTools(server, ctx, env = defaultSkillsEnv()) {
|
|
|
33
38
|
const lines = [
|
|
34
39
|
`Could not read the latest Gameye skills pack version from GitHub: ${err instanceof Error ? err.message : String(err)}`,
|
|
35
40
|
"Nothing was updated.",
|
|
41
|
+
bundled,
|
|
36
42
|
];
|
|
37
43
|
for (const install of installs)
|
|
38
44
|
lines.push(`• ${describeInstall(install)}`);
|
|
39
45
|
return { text: lines.join("\n"), isError: true };
|
|
40
46
|
}
|
|
41
|
-
const lines = [`Latest Gameye skills pack: ${latest}
|
|
47
|
+
const lines = [`Latest Gameye skills pack: ${latest}`, bundled];
|
|
42
48
|
if (installs.length === 0) {
|
|
43
49
|
lines.push("No skills CLI or Claude Code plugin install of the Gameye skills was found.");
|
|
44
50
|
if (installedVersion) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "./common.js";
|
|
3
|
+
import { BUNDLED_HEADER } from "./bundled-skills.js";
|
|
4
|
+
import { BUNDLED_TEMPLATES, TEMPLATE_ENGINES, TEMPLATE_KINDS } from "../generated/skills.js";
|
|
5
|
+
/** A fence of backticks longer than any backtick run in the code, so the block cannot close early. */
|
|
6
|
+
function fenceFor(code) {
|
|
7
|
+
const longest = Math.max(0, ...[...code.matchAll(/`+/g)].map((m) => m[0].length));
|
|
8
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
9
|
+
}
|
|
10
|
+
function formatTemplate(t) {
|
|
11
|
+
const fence = fenceFor(t.code);
|
|
12
|
+
return [
|
|
13
|
+
BUNDLED_HEADER,
|
|
14
|
+
`Template: ${t.engine}/${t.kind}, from the ${t.skill} skill, section "${t.section}".`,
|
|
15
|
+
...(t.note ? ["", `Note: ${t.note}`] : []),
|
|
16
|
+
"",
|
|
17
|
+
`${fence}${t.language}\n${t.code}${fence}`,
|
|
18
|
+
"",
|
|
19
|
+
`Read the rest of the skill with gameye_get_skill (name: "${t.skill}") before adapting this template.`,
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
export function registerTemplateTools(server, ctx) {
|
|
23
|
+
defineTool(server, ctx, "gameye_get_template", {
|
|
24
|
+
title: "Get an engine's Dockerfile or shutdown-hook template",
|
|
25
|
+
description: "Get the Dockerfile or the shutdown-hook template for an Unreal, Unity, Godot or web game server, word for word as published in the bundled Gameye engine skill, with the skill and section it came from. " +
|
|
26
|
+
"Works offline. Read the rest of the skill with gameye_get_skill. If an installed copy of the Gameye skills is newer, it wins.",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
engine: z.enum(TEMPLATE_ENGINES).describe("Game server engine: unreal, unity, godot or web"),
|
|
29
|
+
kind: z.enum(TEMPLATE_KINDS).describe("Template: dockerfile or shutdown-hook"),
|
|
30
|
+
},
|
|
31
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
32
|
+
requiresToken: false,
|
|
33
|
+
}, async ({ engine, kind }) => {
|
|
34
|
+
const template = BUNDLED_TEMPLATES.find((t) => t.engine === engine && t.kind === kind);
|
|
35
|
+
// The bundler fails the build unless every engine and kind is present.
|
|
36
|
+
if (!template)
|
|
37
|
+
throw new Error(`No bundled ${engine}/${kind} template`);
|
|
38
|
+
return formatTemplate(template);
|
|
39
|
+
});
|
|
40
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -156,6 +156,7 @@ export interface CreateTokenRequest {
|
|
|
156
156
|
expires_after?: string;
|
|
157
157
|
}
|
|
158
158
|
export interface GameyeClientConfig {
|
|
159
|
+
/** API token; empty in no-token mode, where the tool gate keeps every request from being sent. */
|
|
159
160
|
token: string;
|
|
160
161
|
baseUrl: string;
|
|
161
162
|
/** Default per-request timeout in milliseconds. Logs and artifacts get at least 60s and 120s. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gameye-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.Gameye/gameye-mcp",
|
|
5
5
|
"description": "MCP server for the Gameye game server orchestration API: launch and inspect sessions, check image tags, fetch logs and artifacts from any MCP client",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"test:smoke": "vitest run --config vitest.smoke.config.ts",
|
|
25
25
|
"sync-spec": "node scripts/sync-spec.mjs",
|
|
26
26
|
"check-spec": "node scripts/sync-spec.mjs --check",
|
|
27
|
+
"sync-skills": "node scripts/sync-skills.mjs",
|
|
28
|
+
"check-skills": "node scripts/sync-skills.mjs --check",
|
|
27
29
|
"prepublishOnly": "npm run build"
|
|
28
30
|
},
|
|
29
31
|
"keywords": [
|