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 +31 -12
- package/dist/env.d.ts +0 -6
- package/dist/env.js +0 -8
- package/dist/index.js +3 -4
- package/dist/skills.d.ts +32 -0
- package/dist/skills.js +140 -0
- package/dist/tools/applications.d.ts +2 -2
- package/dist/tools/applications.js +3 -5
- package/dist/tools/common.d.ts +0 -5
- package/dist/tools/context.d.ts +2 -2
- package/dist/tools/context.js +2 -3
- package/dist/tools/register.d.ts +3 -4
- package/dist/tools/register.js +7 -6
- package/dist/tools/skills.d.ts +4 -0
- package/dist/tools/skills.js +96 -0
- package/dist/tools/tokens.d.ts +0 -1
- package/dist/tools/tokens.js +0 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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`
|
|
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
|
-
|
|
|
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,
|
|
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` |
|
|
132
|
-
| `gameye_edit_application` |
|
|
133
|
-
| `gameye_create_token` |
|
|
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
|
|
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
|
-
##
|
|
164
|
+
## Application and token tools
|
|
154
165
|
|
|
155
|
-
`gameye_create_application`, `gameye_edit_application` and `gameye_create_token` are registered
|
|
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 {
|
|
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
|
-
|
|
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}]
|
|
24
|
+
console.error(`Gameye MCP server ${version} running on stdio [${ctx.environment} · ${ctx.host}]`);
|
|
26
25
|
console.error(BETA_NOTICE);
|
package/dist/skills.d.ts
ADDED
|
@@ -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
|
|
4
|
-
export declare function registerApplicationTools(server: McpServer, client: GameyeClient, ctx: ServerContext
|
|
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
|
|
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
|
-
|
|
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
|
|
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. " +
|
package/dist/tools/common.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/context.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { type
|
|
3
|
-
export declare function registerContextTool(server: McpServer, ctx: ServerContext
|
|
2
|
+
import { type ServerContext } from "./common.js";
|
|
3
|
+
export declare function registerContextTool(server: McpServer, ctx: ServerContext): void;
|
package/dist/tools/context.js
CHANGED
|
@@ -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
|
|
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
|
|
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`,
|
package/dist/tools/register.d.ts
CHANGED
|
@@ -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 {
|
|
4
|
-
|
|
5
|
-
|
|
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;
|
package/dist/tools/register.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
20
|
+
registerApplicationTools(server, client, ctx);
|
|
20
21
|
registerLogTools(server, client, ctx);
|
|
21
22
|
registerArtifactTools(server, client, ctx);
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/dist/tools/tokens.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/tokens.js
CHANGED
|
@@ -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.
|
|
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",
|