gameye-mcp 0.1.3 → 0.3.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 CHANGED
@@ -30,7 +30,6 @@ The environment is a property of the server process. No tool takes an environmen
30
30
  | `GAMEYE_API_TOKEN` | **Yes** | — | Gameye API token. Use the least-privileged scopes you need (see below). |
31
31
  | `GAMEYE_ENV` | No | `sandbox` | `sandbox` or `production`. Selects the API host. |
32
32
  | `GAMEYE_API_URL` | No | — | Overrides the host, for testing only. Must be `https`. If it equals one of the two known hosts, it must match `GAMEYE_ENV` or the server refuses to start. Any other URL is labelled `custom` in every tool result. |
33
- | `GAMEYE_ENABLE_ADMIN_TOOLS` | No | unset | Set to `1` to register the admin tools (create or edit applications, create tokens). They stay off when this is unset. |
34
33
  | `GAMEYE_TIMEOUT_MS` | No | `30000` | Per-request timeout in milliseconds, a positive whole number. Log snapshots get at least 60 s and artifact downloads at least 120 s. A request that runs out of time comes back as a tool error: `Request to <host> timed out after <n>s`. |
35
34
 
36
35
  ## Install
@@ -64,7 +63,7 @@ Most clients (Claude Desktop, Cursor, Windsurf and others) accept an `mcpServers
64
63
 
65
64
  Restart the client after you change its configuration.
66
65
 
67
- `npx -y gameye-mcp` runs the latest release. To stay on one version, name it: `npx -y gameye-mcp@0.1.3` (or `"args": ["-y", "gameye-mcp@0.1.3"]`).
66
+ `npx -y gameye-mcp` runs the latest release. To stay on one version, name it: `npx -y gameye-mcp@0.3.0` (or `"args": ["-y", "gameye-mcp@0.3.0"]`).
68
67
 
69
68
  ### Docker
70
69
 
@@ -89,10 +88,10 @@ With Claude Code: `claude mcp add gameye --env GAMEYE_API_TOKEN=<your-token> --
89
88
 
90
89
  - Pass each variable with `-e NAME` (no value), so Docker copies it from the client's environment and the token never appears in the command line.
91
90
  - Keep `-i` and never add `-t`: a terminal mangles the JSON-RPC messages on stdout.
92
- - To stay on one version, use a version tag such as `ghcr.io/gameye/gameye-mcp:0.1.3`.
91
+ - To stay on one version, use a version tag such as `ghcr.io/gameye/gameye-mcp:0.3.0`.
93
92
  - Files the server writes stay inside the container, and `--rm` deletes them:
94
93
  - `gameye_get_artifacts` saves its download under the temp directory. To keep it, mount a directory and point the temp directory at it: `-v /absolute/dir:/out -e TMPDIR=/out`. The path the tool reports is the container path; the file is in `/absolute/dir` on the host.
95
- - `gameye_create_token` (admin tools only) writes to the `outputPath` you give it. Mount a directory the same way and pass an `outputPath` under `/out`.
94
+ - `gameye_create_token` writes to the `outputPath` you give it. Mount a directory the same way and pass an `outputPath` under `/out`.
96
95
  - The server runs as uid 1000, so on Linux the mounted directory must be writable by that user.
97
96
 
98
97
  ## Token scopes
@@ -103,7 +102,8 @@ Give the server a token with only the scopes it needs.
103
102
  |---|---|---|
104
103
  | Observer | `session:read` `logs:read` `artifact:read` `regions:read` `region:read` `application:read` | Inspect sessions, logs, artifacts, regions and applications. No changes. |
105
104
  | Operator | Observer + `session:start` `session:stop` `session:write` | Also launch and stop sessions and update session state. |
106
- | Admin tools | Operator + `application:write` `token:write` | Also create or edit applications and create tokens. Requires `GAMEYE_ENABLE_ADMIN_TOOLS=1`. |
105
+ | Deployer | Operator + `application:write` | Also create and edit applications, so the agent can set up a deployment end to end. |
106
+ | Token admin | Deployer + `token:write` | Also create API tokens. |
107
107
 
108
108
  **Never pass your Gameye API token into a game server's environment.** A game server doesn't need it, and anything inside the container can read it.
109
109
 
@@ -113,7 +113,7 @@ Every tool result starts with the environment and host it used, for example `[sa
113
113
 
114
114
  | Tool | Kind | Description |
115
115
  |---|---|---|
116
- | `gameye_context` | read | Show the environment, host and server version this server targets, whether admin tools are enabled, and the guardrail defaults (session TTL, `launched-by` label, token expiry) |
116
+ | `gameye_context` | read | Show the environment, host and server version this server targets, and the guardrail defaults (session TTL, `launched-by` label, token expiry) |
117
117
  | `gameye_launch_session` | write, `dryRun` | Start a containerized game server (billable). Default TTL and agent label applied; see [Guardrails](#guardrails) |
118
118
  | `gameye_list_sessions` | read | List running sessions (filterable; `all=true` for every status) |
119
119
  | `gameye_describe_session` | read | Get session details, ports and players |
@@ -128,9 +128,10 @@ Every tool result starts with the environment and host it used, for example `[sa
128
128
  | `gameye_enable_tag` | write, `dryRun` | Enable an image tag for an application so Gameye preloads it |
129
129
  | `gameye_get_logs` | read | Fetch session logs |
130
130
  | `gameye_get_artifacts` | read | Download a file or directory from a session container as a tar archive |
131
- | `gameye_create_application` | admin, `dryRun` | Create an application. Only with `GAMEYE_ENABLE_ADMIN_TOOLS=1` |
132
- | `gameye_edit_application` | admin, destructive, `dryRun` | Change an application; omitted fields stay unchanged. Only with `GAMEYE_ENABLE_ADMIN_TOOLS=1` |
133
- | `gameye_create_token` | admin, `dryRun` | Create an API token and write it to a file. Only with `GAMEYE_ENABLE_ADMIN_TOOLS=1` |
131
+ | `gameye_create_application` | write, `dryRun` | Create an application |
132
+ | `gameye_edit_application` | destructive, `dryRun` | Change an application; omitted fields stay unchanged |
133
+ | `gameye_create_token` | write, `dryRun` | Create an API token and write it to a file |
134
+ | `gameye_update_skills` | read, or write with `apply` | Compare the installed [Gameye skills](https://github.com/gameye/gameye-skills) with the latest pack, and update them with `apply: true`. See [Keeping the skills current](#keeping-the-skills-current) |
134
135
 
135
136
  Every tool carries MCP annotations: read tools set `readOnlyHint`, `gameye_stop_session` and `gameye_edit_application` set `destructiveHint`, and every tool that calls the Gameye API sets `openWorldHint`.
136
137
 
@@ -140,19 +141,29 @@ The tool set covers every operation in the Gameye client API. `tests/parity.test
140
141
 
141
142
  These behave the same on sandbox and production, because sandbox runs live matches for self-serve customers.
142
143
 
143
- - **Dry run.** `gameye_launch_session`, `gameye_stop_session`, `gameye_enable_tag` and the admin tools accept `dryRun: true`. The tool returns the exact request (method, host, path and body) with the API token masked, and sends nothing.
144
+ - **Dry run.** `gameye_launch_session`, `gameye_stop_session`, `gameye_enable_tag` and the application and token tools accept `dryRun: true`. The tool returns the exact request (method, host, path and body) with the API token masked, and sends nothing.
144
145
  - **Bounded sessions.** A launch without `ttl` gets a `30m` TTL, and the result says the default was applied. Every launch sends the label `launched-by: gameye-mcp`, replacing any `launched-by` value the caller passes, so agent-launched sessions can be found with `gameye_list_sessions` and `filter: { "launched-by": "gameye-mcp" }`. When a warm pool serves the image and region, the Gameye API ignores request labels (and env and args), so warm-pool sessions do not carry this label.
145
146
  - **No token in game servers.** A launch is refused, with nothing sent, if any env value or name, arg, or label key or value contains this server's Gameye API token. Env values are never shown in results.
146
147
  - **No token in results.** Every tool result, including errors, has this server's API token replaced with `***`, in case log output, labels or an error message echo it.
147
148
  - **Explicit stops.** `gameye_stop_session` stops exactly one session by its exact ID. There is no bulk stop, and empty, wildcard or list IDs are refused.
148
149
 
150
+ ## Keeping the skills current
151
+
152
+ The [Gameye skills](https://github.com/gameye/gameye-skills) change as the Gameye API does. `gameye_update_skills` reads the latest pack version from the `gameye` skill on GitHub (`Pack version:` in `skills/gameye/SKILL.md` on `main`) and compares it with what is installed:
153
+
154
+ - **skills CLI, user-wide:** skills recorded in `~/.agents/.skill-lock.json` with source `gameye/gameye-skills`.
155
+ - **skills CLI, this project:** the same, in `skills-lock.json` in the server's working directory.
156
+ - **Claude Code plugin:** `gameye@gameye` in `~/.claude/plugins/installed_plugins.json`.
157
+
158
+ Without `apply` it only reports. With `apply: true` it runs each outdated install's own updater: `npx -y skills update <the Gameye skills> -g -y` (or `-p -y`), or `claude plugin marketplace update gameye` then `claude plugin update gameye@gameye`. It only updates skills whose lock-file source is `gameye/gameye-skills`, and it never downloads or follows skill text itself. Each command runs with stdin closed and a 90-second limit. On Windows it prints the commands instead of running them. The running agent session keeps the skills it loaded; the next session uses the update. The `gameye` skill tells agents to run the check once per conversation and ask before applying.
159
+
149
160
  ## Telemetry/attribution
150
161
 
151
162
  Every request to the Gameye API sends `User-Agent: gameye-mcp/<version>`, plus the MCP client's name once it has connected, e.g. `gameye-mcp/0.1.0 (claude-code)`, so Gameye can count MCP usage. Only this server's version and the client name are sent, nothing else: no token, env values, paths or other data. There is no other telemetry.
152
163
 
153
- ## Admin tools
164
+ ## Application and token tools
154
165
 
155
- `gameye_create_application`, `gameye_edit_application` and `gameye_create_token` are registered only when the server starts with `GAMEYE_ENABLE_ADMIN_TOOLS=1`. Without it, they don't appear in the tool list at all. They need a token with `application:write` or `token:write`.
166
+ `gameye_create_application`, `gameye_edit_application` and `gameye_create_token` are always registered. What the agent can actually do is set by the token's scopes: they need `application:write` or `token:write`, and a token without them gets a 403 naming the missing scope. To keep an agent from changing applications or minting tokens, give it a token without those scopes.
156
167
 
157
168
  `gameye_create_token` never returns the new token. It writes the token to the file you name in `outputPath` and returns only the path, the scopes and the expiry:
158
169
 
@@ -185,6 +196,14 @@ npm run build
185
196
 
186
197
  `spec/client.yaml` is a snapshot of the public sandbox `/openapi.yaml`, and `spec/overrides.yaml` holds the MCP tool name for each operation: the `tools:` map from [gameye-skills](https://github.com/gameye/gameye-skills)' `spec/overrides.yaml`, reduced by `scripts/reduce-overrides.mjs` (gameye-skills keeps the full file, which records where the server differs from the spec). Refresh both with `npm run sync-spec [path-or-url-to-overrides.yaml]`. `npm run check-spec` exits 1 when either snapshot has drifted.
187
198
 
199
+ ## About Gameye
200
+
201
+ [Gameye](https://gameye.com/?utm_source=github&utm_medium=readme&utm_campaign=gameye-mcp) is a managed hosting platform for multiplayer game servers. You bring a container image, and Gameye starts a dedicated server for each match through one API.
202
+
203
+ - Session API docs: [gameye.com/docs/api-v2](https://gameye.com/docs/api-v2/?utm_source=github&utm_medium=readme&utm_campaign=gameye-mcp)
204
+ - Gameye docs: [gameye.com/docs](https://gameye.com/docs/?utm_source=github&utm_medium=readme&utm_campaign=gameye-mcp)
205
+ - Try it free: [trial.gameye.com](https://trial.gameye.com/?utm_source=github&utm_medium=readme&utm_campaign=gameye-mcp)
206
+
188
207
  ## Security
189
208
 
190
209
  Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md).
package/dist/env.d.ts CHANGED
@@ -34,9 +34,3 @@ export declare function resultPrefix(config: {
34
34
  environment: EnvironmentLabel;
35
35
  host: string;
36
36
  }): string;
37
- /**
38
- * Admin tools (create/edit application, create token) register only when
39
- * GAMEYE_ENABLE_ADMIN_TOOLS is exactly `1`, so credential- and config-changing
40
- * tools are invisible unless the operator opts in.
41
- */
42
- export declare function adminToolsEnabled(env: Record<string, string | undefined>): boolean;
package/dist/env.js CHANGED
@@ -112,11 +112,3 @@ export function loadConfigOrExit(env = process.env) {
112
112
  export function resultPrefix(config) {
113
113
  return `[${config.environment} · ${config.host}]`;
114
114
  }
115
- /**
116
- * Admin tools (create/edit application, create token) register only when
117
- * GAMEYE_ENABLE_ADMIN_TOOLS is exactly `1`, so credential- and config-changing
118
- * tools are invisible unless the operator opts in.
119
- */
120
- export function adminToolsEnabled(env) {
121
- return env.GAMEYE_ENABLE_ADMIN_TOOLS?.trim() === "1";
122
- }
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { GameyeClient } from "./client.js";
4
- import { adminToolsEnabled, loadConfigOrExit } from "./env.js";
4
+ import { loadConfigOrExit } from "./env.js";
5
5
  import { registerAllTools } from "./tools/register.js";
6
6
  import { createServer } from "./server.js";
7
7
  import { BETA_NOTICE } from "./beta.js";
@@ -16,11 +16,10 @@ const ctx = {
16
16
  mask: (text) => client.maskToken(text),
17
17
  };
18
18
  const server = createServer(version);
19
- const admin = adminToolsEnabled(process.env);
20
- registerAllTools(server, client, ctx, { admin });
19
+ registerAllTools(server, client, ctx);
21
20
  // Attribute API traffic to the MCP client (name only) once it has introduced itself.
22
21
  server.server.oninitialized = () => client.setClientName(server.server.getClientVersion()?.name);
23
22
  const transport = new StdioServerTransport();
24
23
  await server.connect(transport);
25
- console.error(`Gameye MCP server ${version} running on stdio [${ctx.environment} · ${ctx.host}]${admin ? " with admin tools enabled" : ""}`);
24
+ console.error(`Gameye MCP server ${version} running on stdio [${ctx.environment} · ${ctx.host}]`);
26
25
  console.error(BETA_NOTICE);
@@ -0,0 +1,32 @@
1
+ /** Where the `gameye` skill's `Pack version:` line is published. */
2
+ export declare const LATEST_PACK_URL = "https://raw.githubusercontent.com/Gameye/gameye-skills/main/skills/gameye/SKILL.md";
3
+ /** How the Gameye skills were installed. Each kind updates differently. */
4
+ export type InstallKind = "skills-cli-global" | "skills-cli-project" | "claude-plugin";
5
+ export interface SkillsInstall {
6
+ kind: InstallKind;
7
+ /** Pack version read from the installed `gameye` skill, if it could be read. */
8
+ version?: string;
9
+ /** Skill names installed from gameye-skills (skills CLI installs only). */
10
+ skills: string[];
11
+ }
12
+ /** Everything the check touches outside this module, so tests can replace it. */
13
+ export interface SkillsEnv {
14
+ home: string;
15
+ cwd: string;
16
+ platform: NodeJS.Platform;
17
+ readText: (path: string) => Promise<string | undefined>;
18
+ fetchText: (url: string) => Promise<string>;
19
+ run: (cmd: string, args: string[], cwd: string) => Promise<string>;
20
+ }
21
+ export declare function defaultSkillsEnv(): SkillsEnv;
22
+ export declare function stripAnsi(text: string): string;
23
+ export declare function parsePackVersion(skillText: string | undefined): string | undefined;
24
+ /** -1, 0 or 1 for two X.Y.Z versions. */
25
+ export declare function compareVersions(a: string, b: string): number;
26
+ /** Find every install of the Gameye skills this server can see: user-wide, this project, and the Claude Code plugin. */
27
+ export declare function findSkillsInstalls(env: SkillsEnv): Promise<SkillsInstall[]>;
28
+ export declare function fetchLatestPackVersion(env: SkillsEnv): Promise<string>;
29
+ export declare function isOutdated(install: SkillsInstall, latest: string): boolean;
30
+ /** The commands that update an install, as argv lists. The skills CLI updates only the named Gameye skills. */
31
+ export declare function updateCommands(install: SkillsInstall): string[][];
32
+ export declare function describeInstall(install: SkillsInstall): string;
package/dist/skills.js ADDED
@@ -0,0 +1,140 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ /** Where the `gameye` skill's `Pack version:` line is published. */
6
+ export const LATEST_PACK_URL = "https://raw.githubusercontent.com/Gameye/gameye-skills/main/skills/gameye/SKILL.md";
7
+ const PACK_VERSION_RE = /^Pack version: (\d+\.\d+\.\d+)[ \t]*$/m;
8
+ const SKILLS_SOURCE_RE = /^(https:\/\/github\.com\/|git@github\.com:|github:)?gameye\/gameye-skills(\.git)?\/?$/i;
9
+ // Lock files can come from a cloned project, so a name must look like a skill name, never a CLI option.
10
+ const SKILL_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
11
+ export function defaultSkillsEnv() {
12
+ return {
13
+ home: homedir(),
14
+ cwd: process.cwd(),
15
+ platform: process.platform,
16
+ readText: (path) => readFile(path, "utf8").catch(() => undefined),
17
+ fetchText: async (url) => {
18
+ const res = await fetch(url, { signal: AbortSignal.timeout(5_000) });
19
+ if (!res.ok)
20
+ throw new Error(`GET ${url} returned ${res.status}`);
21
+ return res.text();
22
+ },
23
+ run: (cmd, args, cwd) => new Promise((resolve, reject) => {
24
+ const childEnv = { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0", CI: "1" };
25
+ const options = { cwd, env: childEnv, timeout: 90_000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 };
26
+ const child = execFile(cmd, args, options, (err, stdout, stderr) => {
27
+ const out = stripAnsi(`${stdout}${stderr}`).trim();
28
+ if (err)
29
+ reject(new Error(`${cmd} ${args.join(" ")} failed: ${err.message}\n${out}`));
30
+ else
31
+ resolve(out);
32
+ });
33
+ // Closed stdin makes a prompting CLI fail at once instead of waiting for the timeout.
34
+ child.stdin?.end();
35
+ }),
36
+ };
37
+ }
38
+ // Terminal colour and cursor codes and carriage returns, which some CLIs print even when asked not to.
39
+ const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]|\r/g;
40
+ export function stripAnsi(text) {
41
+ return text.replace(ANSI_RE, "");
42
+ }
43
+ export function parsePackVersion(skillText) {
44
+ return skillText ? PACK_VERSION_RE.exec(skillText)?.[1] : undefined;
45
+ }
46
+ /** -1, 0 or 1 for two X.Y.Z versions. */
47
+ export function compareVersions(a, b) {
48
+ const pa = a.split(".").map(Number);
49
+ const pb = b.split(".").map(Number);
50
+ for (let i = 0; i < 3; i++) {
51
+ if (pa[i] !== pb[i])
52
+ return pa[i] < pb[i] ? -1 : 1;
53
+ }
54
+ return 0;
55
+ }
56
+ function parseJson(text) {
57
+ if (!text)
58
+ return undefined;
59
+ try {
60
+ return JSON.parse(text);
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /** Skill names a skills CLI lock file records as coming from gameye-skills. */
67
+ function gameyeSkillsInLock(lockText) {
68
+ const lock = parseJson(lockText);
69
+ return Object.entries(lock?.skills ?? {})
70
+ .filter(([name, s]) => SKILL_NAME_RE.test(name) && (SKILLS_SOURCE_RE.test(s.source ?? "") || SKILLS_SOURCE_RE.test(s.sourceUrl ?? "")))
71
+ .map(([name]) => name)
72
+ .sort();
73
+ }
74
+ // The skills CLI keeps one copy in .agents/skills when it installs for several agents,
75
+ // and copies straight into the agent's own folder when it installs for one.
76
+ const SKILL_DIRS = [".agents/skills", ".claude/skills", ".codex/skills", ".cursor/skills", ".config/opencode/skills"];
77
+ /** The oldest pack version among the copies found, so one stale copy still counts as outdated. */
78
+ async function readInstalledPackVersion(env, root) {
79
+ const versions = [];
80
+ for (const dir of SKILL_DIRS) {
81
+ const version = parsePackVersion(await env.readText(join(root, dir, "gameye", "SKILL.md")));
82
+ if (version)
83
+ versions.push(version);
84
+ }
85
+ return versions.sort(compareVersions)[0];
86
+ }
87
+ async function skillsCliInstall(env, kind, lockPath, root) {
88
+ const skills = gameyeSkillsInLock(await env.readText(lockPath));
89
+ if (skills.length === 0)
90
+ return undefined;
91
+ return { kind, version: await readInstalledPackVersion(env, root), skills };
92
+ }
93
+ async function claudePluginInstall(env) {
94
+ const registry = parseJson(await env.readText(join(env.home, ".claude", "plugins", "installed_plugins.json")));
95
+ const entry = Object.entries(registry?.plugins ?? {}).find(([id]) => id === "gameye@gameye")?.[1]?.[0];
96
+ if (!entry?.installPath)
97
+ return undefined;
98
+ const version = parsePackVersion(await env.readText(join(entry.installPath, "skills", "gameye", "SKILL.md")));
99
+ return { kind: "claude-plugin", version, skills: [] };
100
+ }
101
+ /** Find every install of the Gameye skills this server can see: user-wide, this project, and the Claude Code plugin. */
102
+ export async function findSkillsInstalls(env) {
103
+ const found = await Promise.all([
104
+ skillsCliInstall(env, "skills-cli-global", join(env.home, ".agents", ".skill-lock.json"), env.home),
105
+ skillsCliInstall(env, "skills-cli-project", join(env.cwd, "skills-lock.json"), env.cwd),
106
+ claudePluginInstall(env),
107
+ ]);
108
+ return found.filter((i) => i !== undefined);
109
+ }
110
+ export async function fetchLatestPackVersion(env) {
111
+ const version = parsePackVersion(await env.fetchText(LATEST_PACK_URL));
112
+ if (!version)
113
+ throw new Error(`No "Pack version: X.Y.Z" line in ${LATEST_PACK_URL}`);
114
+ return version;
115
+ }
116
+ export function isOutdated(install, latest) {
117
+ return !install.version || compareVersions(install.version, latest) < 0;
118
+ }
119
+ /** The commands that update an install, as argv lists. The skills CLI updates only the named Gameye skills. */
120
+ export function updateCommands(install) {
121
+ switch (install.kind) {
122
+ case "skills-cli-global":
123
+ return [["npx", "-y", "skills", "update", ...install.skills, "-g", "-y"]];
124
+ case "skills-cli-project":
125
+ return [["npx", "-y", "skills", "update", ...install.skills, "-p", "-y"]];
126
+ case "claude-plugin":
127
+ return [
128
+ ["claude", "plugin", "marketplace", "update", "gameye"],
129
+ ["claude", "plugin", "update", "gameye@gameye"],
130
+ ];
131
+ }
132
+ }
133
+ export function describeInstall(install) {
134
+ const where = {
135
+ "skills-cli-global": "skills CLI, user-wide",
136
+ "skills-cli-project": "skills CLI, this project",
137
+ "claude-plugin": "Claude Code plugin gameye@gameye",
138
+ }[install.kind];
139
+ return `${where}: pack ${install.version ?? "version unknown"}`;
140
+ }
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { GameyeClient } from "../client.js";
3
- import { type RegisterOptions, type ServerContext } from "./common.js";
4
- export declare function registerApplicationTools(server: McpServer, client: GameyeClient, ctx: ServerContext, opts: RegisterOptions): void;
3
+ import { type ServerContext } from "./common.js";
4
+ export declare function registerApplicationTools(server: McpServer, client: GameyeClient, ctx: ServerContext): void;
@@ -49,7 +49,7 @@ function formatApplication(a) {
49
49
  ` ports: ${formatPortBindings(a.ports) || "(none)"}`,
50
50
  ].join("\n");
51
51
  }
52
- export function registerApplicationTools(server, client, ctx, opts) {
52
+ export function registerApplicationTools(server, client, ctx) {
53
53
  // ─── List applications ───────────────────────────────────────────────────────
54
54
  defineTool(server, ctx, "gameye_list_applications", {
55
55
  title: "List applications",
@@ -97,9 +97,7 @@ export function registerApplicationTools(server, client, ctx, opts) {
97
97
  await client.enableTag(name, tag);
98
98
  return `Tag ${tag} enabled for ${name}. Preloading can take a while; use gameye_check_tag to confirm it is available in a region.`;
99
99
  });
100
- if (!opts.admin)
101
- return;
102
- // ─── Create application (admin) ──────────────────────────────────────────────
100
+ // ─── Create application ──────────────────────────────────────────────────────
103
101
  defineTool(server, ctx, "gameye_create_application", {
104
102
  title: "Create application",
105
103
  description: "Admin tool. Create a new application (an image Gameye can run). " +
@@ -117,7 +115,7 @@ export function registerApplicationTools(server, client, ctx, opts) {
117
115
  await client.createApplication(body);
118
116
  return `Application ${body.name} created.`;
119
117
  });
120
- // ─── Edit application (admin) ────────────────────────────────────────────────
118
+ // ─── Edit application ────────────────────────────────────────────────────────
121
119
  defineTool(server, ctx, "gameye_edit_application", {
122
120
  title: "Edit application",
123
121
  description: "Admin tool. Change an existing application with PUT /application/{name}. Fields you omit stay unchanged; regions and ports, when given, replace the whole list. " +
@@ -5,11 +5,6 @@ import { GameyeError } from "../client.js";
5
5
  import { type EnvironmentLabel } from "../env.js";
6
6
  /** Shared `dryRun` input for every tool that sends a write. */
7
7
  export declare const dryRunInput: z.ZodOptional<z.ZodBoolean>;
8
- /** Tool registration options. Admin tools only when GAMEYE_ENABLE_ADMIN_TOOLS=1. */
9
- export interface RegisterOptions {
10
- /** Register admin tools (create/edit application, create token). */
11
- admin: boolean;
12
- }
13
8
  /** What every tool needs to know about the server it runs in. Never holds the token. */
14
9
  export interface ServerContext {
15
10
  environment: EnvironmentLabel;
@@ -1,3 +1,3 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { type RegisterOptions, type ServerContext } from "./common.js";
3
- export declare function registerContextTool(server: McpServer, ctx: ServerContext, opts: RegisterOptions): void;
2
+ import { type ServerContext } from "./common.js";
3
+ export declare function registerContextTool(server: McpServer, ctx: ServerContext): void;
@@ -1,10 +1,10 @@
1
1
  import { AGENT_LABEL_KEY, AGENT_LABEL_VALUE, DEFAULT_SESSION_TTL, DEFAULT_TOKEN_EXPIRY } from "../guards.js";
2
2
  import { defineTool } from "./common.js";
3
3
  import { BETA_NOTICE } from "../beta.js";
4
- export function registerContextTool(server, ctx, opts) {
4
+ export function registerContextTool(server, ctx) {
5
5
  defineTool(server, ctx, "gameye_context", {
6
6
  title: "Server context",
7
- description: "Show which Gameye environment and host this server targets, the server version, whether admin tools are enabled, and the guardrail defaults. The environment is fixed when the server starts (GAMEYE_ENV / GAMEYE_API_URL) and cannot be changed by a tool call.",
7
+ description: "Show which Gameye environment and host this server targets, the server version and the guardrail defaults. The environment is fixed when the server starts (GAMEYE_ENV / GAMEYE_API_URL) and cannot be changed by a tool call.",
8
8
  inputSchema: {},
9
9
  annotations: { readOnlyHint: true, openWorldHint: false },
10
10
  }, async () => [
@@ -13,7 +13,6 @@ export function registerContextTool(server, ctx, opts) {
13
13
  `Host: ${ctx.host}`,
14
14
  `Base URL: ${ctx.baseUrl}`,
15
15
  `Server version: gameye-mcp ${ctx.version}`,
16
- `Admin tools: ${opts.admin ? "enabled" : "disabled (set GAMEYE_ENABLE_ADMIN_TOOLS=1 to enable)"}`,
17
16
  "",
18
17
  "Guardrail defaults:",
19
18
  ` Session TTL: ${DEFAULT_SESSION_TTL} when a launch gives no ttl`,
@@ -1,6 +1,5 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { GameyeClient } from "../client.js";
3
- import type { RegisterOptions, ServerContext } from "./common.js";
4
- export type { RegisterOptions };
5
- /** Register every tool. Admin tools only when `opts.admin` is true. */
6
- export declare function registerAllTools(server: McpServer, client: GameyeClient, ctx: ServerContext, opts: RegisterOptions): void;
3
+ import type { ServerContext } from "./common.js";
4
+ /** Register every tool. */
5
+ export declare function registerAllTools(server: McpServer, client: GameyeClient, ctx: ServerContext): void;
@@ -8,17 +8,18 @@ import { registerApplicationTools } from "./applications.js";
8
8
  import { registerTokenTools } from "./tokens.js";
9
9
  import { registerLogTools } from "./logs.js";
10
10
  import { registerArtifactTools } from "./artifacts.js";
11
- /** Register every tool. Admin tools only when `opts.admin` is true. */
12
- export function registerAllTools(server, client, ctx, opts) {
13
- registerContextTool(server, ctx, opts);
11
+ import { registerSkillsTools } from "./skills.js";
12
+ /** Register every tool. */
13
+ export function registerAllTools(server, client, ctx) {
14
+ registerContextTool(server, ctx);
14
15
  registerSessionTools(server, client, ctx);
15
16
  registerPlayerTools(server, client, ctx);
16
17
  registerLocationTools(server, client, ctx);
17
18
  registerRegionTools(server, client, ctx);
18
19
  registerTagTools(server, client, ctx);
19
- registerApplicationTools(server, client, ctx, opts);
20
+ registerApplicationTools(server, client, ctx);
20
21
  registerLogTools(server, client, ctx);
21
22
  registerArtifactTools(server, client, ctx);
22
- if (opts.admin)
23
- registerTokenTools(server, client, ctx);
23
+ registerTokenTools(server, client, ctx);
24
+ registerSkillsTools(server, ctx);
24
25
  }
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type ServerContext } from "./common.js";
3
+ import { type SkillsEnv } from "../skills.js";
4
+ export declare function registerSkillsTools(server: McpServer, ctx: ServerContext, env?: SkillsEnv): void;
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "./common.js";
3
+ import { compareVersions, defaultSkillsEnv, describeInstall, fetchLatestPackVersion, findSkillsInstalls, isOutdated, updateCommands, } from "../skills.js";
4
+ 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
+ export function registerSkillsTools(server, ctx, env = defaultSkillsEnv()) {
6
+ defineTool(server, ctx, "gameye_update_skills", {
7
+ title: "Check and update the Gameye skills",
8
+ description: "Compare the installed Gameye agent skills with the latest pack on GitHub, and update them. " +
9
+ "Finds installs made with the skills CLI (user-wide or in this project) and the Claude Code plugin gameye@gameye. " +
10
+ "Without apply it only checks and changes nothing. With apply=true it runs each outdated install's own updater " +
11
+ "(npx skills update for the Gameye skills only, or claude plugin update); ask the user before applying. " +
12
+ "Updates come only through those package managers; this tool never downloads or follows skill text itself.",
13
+ inputSchema: {
14
+ apply: z
15
+ .boolean()
16
+ .optional()
17
+ .describe("Run the updates for outdated installs. Omit or false to only check."),
18
+ installedVersion: z
19
+ .string()
20
+ .regex(/^\d+\.\d+\.\d+$/)
21
+ .optional()
22
+ .describe("The `Pack version:` from the gameye skill you have loaded, used when no install is found on disk"),
23
+ },
24
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
25
+ }, async ({ apply, installedVersion }) => {
26
+ const installs = await findSkillsInstalls(env);
27
+ let latest;
28
+ try {
29
+ latest = await fetchLatestPackVersion(env);
30
+ }
31
+ catch (err) {
32
+ const lines = [
33
+ `Could not read the latest Gameye skills pack version from GitHub: ${err instanceof Error ? err.message : String(err)}`,
34
+ "Nothing was updated.",
35
+ ];
36
+ for (const install of installs)
37
+ lines.push(`• ${describeInstall(install)}`);
38
+ return { text: lines.join("\n"), isError: true };
39
+ }
40
+ const lines = [`Latest Gameye skills pack: ${latest}`];
41
+ if (installs.length === 0) {
42
+ lines.push("No skills CLI or Claude Code plugin install of the Gameye skills was found.");
43
+ if (installedVersion) {
44
+ lines.push(`The loaded gameye skill reports pack ${installedVersion}.`);
45
+ if (compareVersions(installedVersion, latest) < 0) {
46
+ lines.push("To update, reinstall the way they were installed: `npx skills add gameye/gameye-skills --skill \"*\"` (add -g for user-wide), " +
47
+ "`/plugin marketplace update gameye` for the Claude Code plugin, or copy the skills directories again.");
48
+ }
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ const outdated = installs.filter((i) => isOutdated(i, latest));
53
+ for (const install of installs) {
54
+ lines.push(`• ${describeInstall(install)}${outdated.includes(install) ? " (update available)" : " (up to date)"}`);
55
+ }
56
+ if (outdated.length === 0)
57
+ return lines.join("\n");
58
+ const commands = outdated.flatMap(updateCommands);
59
+ if (!apply || env.platform === "win32") {
60
+ lines.push("", apply ? "Updating from this server isn't supported on Windows. Run:" : "Call again with apply=true to run:");
61
+ for (const cmd of commands)
62
+ lines.push(` ${cmd.join(" ")}`);
63
+ return lines.join("\n");
64
+ }
65
+ let failed = false;
66
+ lines.push("");
67
+ for (const install of outdated) {
68
+ for (const cmd of updateCommands(install)) {
69
+ try {
70
+ const out = await env.run(cmd[0], cmd.slice(1), env.cwd);
71
+ lines.push(`Ran: ${cmd.join(" ")}`, ...(out ? [indentTail(out)] : []));
72
+ }
73
+ catch (err) {
74
+ failed = true;
75
+ lines.push(`Failed: ${err instanceof Error ? err.message : String(err)}`);
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ const after = await findSkillsInstalls(env);
81
+ lines.push("", "After updating:");
82
+ for (const install of after) {
83
+ const behind = isOutdated(install, latest);
84
+ failed ||= behind;
85
+ lines.push(`• ${describeInstall(install)}${behind ? " (still behind)" : ""}`);
86
+ }
87
+ lines.push("", RELOAD_NOTE);
88
+ return { text: lines.join("\n"), isError: failed };
89
+ });
90
+ }
91
+ /** The last lines of a command's output, indented, so a long install log doesn't flood the result. */
92
+ function indentTail(out, max = 15) {
93
+ const all = out.split("\n");
94
+ const tail = all.slice(-max).map((l) => ` ${l}`);
95
+ return (all.length > max ? [" …", ...tail] : tail).join("\n");
96
+ }
@@ -1,5 +1,4 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { GameyeClient } from "../client.js";
3
3
  import { type ServerContext } from "./common.js";
4
- /** Admin tool. Register only when GAMEYE_ENABLE_ADMIN_TOOLS=1. */
5
4
  export declare function registerTokenTools(server: McpServer, client: GameyeClient, ctx: ServerContext): void;
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  import { GameyeClient } from "../client.js";
3
3
  import { DEFAULT_TOKEN_EXPIRY, GuardError, checkTokenOutputPath, formatDryRun, reserveTokenFile } from "../guards.js";
4
4
  import { defineTool } from "./common.js";
5
- /** Admin tool. Register only when GAMEYE_ENABLE_ADMIN_TOOLS=1. */
6
5
  export function registerTokenTools(server, client, ctx) {
7
6
  defineTool(server, ctx, "gameye_create_token", {
8
7
  title: "Create API token",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gameye-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.3.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",