@edgegap/mcp 0.1.2 → 0.2.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
@@ -1,15 +1,42 @@
1
1
  # edgegap-mcp
2
2
 
3
- An MCP server that lets a coding agent take a developer from "I have a game
4
- server container" to "players are connected to it" without the developer
5
- reading the API reference.
3
+ An MCP server for Edgegap that lets a coding agent take a developer from "I
4
+ have a headless server build" to "players are connected to it" without the
5
+ developer reading the API reference: checking the Dockerfile and ports, pushing
6
+ to Edgegap's registry, deploying, and — for peer-to-peer games — opening a relay
7
+ instead.
6
8
 
7
- Ten tools, hand-picked. Not generated from the OpenAPI spec — see
8
- [Scope](#scope) for why.
9
+ Eighteen tools, hand-picked. Not generated from the OpenAPI spec — see
10
+ [Design decisions](#design-decisions) for why.
9
11
 
10
12
  ## Install
11
13
 
12
- One line in your MCP client config. Nothing to clone, nothing to build.
14
+ Two ways to run it. Pick based on how much you care about where your token
15
+ goes — see [Where your token goes](#where-your-token-goes).
16
+
17
+ ### Remote endpoint
18
+
19
+ Hosted by Edgegap as a Cloudflare Worker. Nothing to install.
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "edgegap": {
25
+ "type": "http",
26
+ "url": "https://mcp.edgegap.dev/mcp",
27
+ "headers": { "Authorization": "token YOUR_API_TOKEN" }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Also works as a custom connector in claude.ai: add
34
+ `https://mcp.edgegap.dev/mcp` and supply the same token.
35
+
36
+ ### Local
37
+
38
+ Runs on your own machine, spawned by your editor. One line in your MCP client
39
+ config, nothing to clone, nothing to build.
13
40
 
14
41
  ```json
15
42
  {
@@ -23,17 +50,20 @@ One line in your MCP client config. Nothing to clone, nothing to build.
23
50
  ```
24
51
 
25
52
  Works in Claude Code, Cursor, Codex, and VS Code. Pin a version in production
26
- (`@edgegap/mcp@0.1.0`) rather than floating on latest.
53
+ (`@edgegap/mcp@0.1.5`) rather than floating on latest.
54
+
55
+ Registered in the official MCP registry as `dev.edgegap/mcp`.
27
56
 
28
- > **Node version:** the server itself needs Node 18+. Deploying the optional
29
- > Cloudflare Worker needs Node 22+, because `wrangler` requires it.
57
+ > **Node version:** the local server needs Node 18+. Deploying your own copy of
58
+ > the Cloudflare Worker needs Node 22+, because `wrangler` requires it.
30
59
 
31
- ## Your token never leaves your machine
60
+ ## Where your token goes
32
61
 
33
- There is no Edgegap-hosted component. This server runs as a process on your
34
- own computer, spawned by your editor. The first tool call asks you for a token,
35
- shows what it authorises, and requires an explicit acknowledgement before
36
- accepting it. Where that token then lives, exhaustively:
62
+ This differs by mode, and the difference is the reason both modes exist.
63
+
64
+ **Local.** The server runs as a process on your own computer. The first tool
65
+ call asks you for a token, shows what it authorises, and requires an explicit
66
+ acknowledgement before accepting it. Where that token then lives, exhaustively:
37
67
 
38
68
  - one variable in that process's memory, for the life of your editor session
39
69
 
@@ -42,18 +72,24 @@ any Edgegap server — the only thing sent to Edgegap is the API call itself,
42
72
  exactly as if you had run `curl`. Closing your editor revokes this server's
43
73
  access completely.
44
74
 
75
+ **Remote.** Your token is sent to `mcp.edgegap.dev` on every request and
76
+ forwarded from there to the Edgegap API. It transits infrastructure Edgegap
77
+ operates. The worker holds it for the life of the request and does not persist
78
+ it, but that is a "we don't store it" claim rather than a "we never see it"
79
+ claim. The two are different, and only local mode makes the second one.
80
+
45
81
  Generate a token at <https://app.edgegap.com/user-settings?tab=tokens>.
46
82
 
47
- Setting `EDGEGAP_API_TOKEN` still works and takes precedence, for CI and for
48
- clients that cannot show prompts. Do not pass a token as a command-line
49
- argument — arguments are visible to other processes via `ps`, and the server
50
- warns if it detects one.
83
+ In local mode, setting `EDGEGAP_API_TOKEN` takes precedence over the prompt,
84
+ for CI and for clients that cannot show prompts. Do not pass a token as a
85
+ command-line argument — arguments are visible to other processes via `ps`, and
86
+ the server warns if it detects one.
51
87
 
52
- **Why this is not hosted.** A hosted server would have to either store your
53
- token or receive it on every request. "We don't store it" and "we never see it"
54
- are different claims, and only a local process makes the second one. See
55
- `worker/DECISION.md` for the full reasoning and the conditions under which a
56
- hosted version becomes worth building.
88
+ **Which to use.** Remote for a first try, a demo, or a supervised session where
89
+ setup friction matters more than custody. Local for anything unattended,
90
+ anything in an organization with a live game in it, and anything where you
91
+ would rather not extend trust you don't have to. The guardrails described below
92
+ exist only in local mode.
57
93
 
58
94
  ## Read this before connecting an agent
59
95
 
@@ -70,53 +106,115 @@ Consequences worth being deliberate about:
70
106
  - Anything the agent logs, echoes, or sends to a model provider is a place the
71
107
  token could end up. This server does not log it, but it cannot control what
72
108
  the rest of the agent does.
109
+ - On the remote endpoint, the same unscoped token is additionally handled by
110
+ Edgegap's worker on every call.
73
111
 
74
112
  Recommended setup, in decreasing order of caution:
75
113
 
76
114
  | Situation | Setup |
77
115
  | --- | --- |
78
- | Unattended or autonomous agent | Separate non-production organization, plus `EDGEGAP_READ_ONLY=1` |
79
- | Supervised agent, live game in the org | `EDGEGAP_APP_ALLOWLIST` scoped to the app being worked on, plus `EDGEGAP_MAX_DURATION_MINUTES` |
80
- | Solo developer, no production workload | Defaults are fine; revoke the token when finished |
116
+ | Unattended or autonomous agent | Local mode. Separate non-production organization, plus `EDGEGAP_READ_ONLY=1` |
117
+ | Supervised agent, live game in the org | Local mode. `EDGEGAP_APP_ALLOWLIST` scoped to the app being worked on, plus `EDGEGAP_MAX_DURATION_MINUTES`. Read [Scope of the allowlist](#scope-of-the-allowlist) first — deployments that are already running are not covered |
118
+ | Solo developer, no production workload | Either mode. Defaults are fine; revoke the token when finished |
119
+
120
+ The allowlist and read-only flag are enforced in the local server, which means
121
+ they protect against an agent that makes a mistake, not against one that has
122
+ been compromised into calling the API directly. They narrow the blast radius;
123
+ they do not remove it.
124
+
125
+ ### Scope of the allowlist
81
126
 
82
- The allowlist and read-only flag are enforced in this server, which means they
83
- protect against an agent that makes a mistake, not against one that has been
84
- compromised into calling the API directly. They narrow the blast radius; they
85
- do not remove it.
127
+ `EDGEGAP_APP_ALLOWLIST` is enforced by the five tools that take an application
128
+ name: `edgegap_create_app`, `edgegap_list_app_versions`,
129
+ `edgegap_create_app_version`, `edgegap_deploy`, and
130
+ `edgegap_build_matchmaker_config`.
131
+
132
+ Relay sessions and the container registry belong to the organization, not to an
133
+ application, so the relay and registry tools are not covered by it either.
134
+
135
+ It is **not** enforced by the five tools keyed on `request_id`:
136
+ `edgegap_get_deployment`, `edgegap_wait_for_deployment`,
137
+ `edgegap_list_deployments`, `edgegap_stop_deployment`, and
138
+ `edgegap_get_deployment_logs`. An agent running with an allowlist set can list
139
+ every deployment in the organization and then inspect, read the logs of, or stop
140
+ any of them — including deployments belonging to applications outside the list.
141
+
142
+ So the allowlist scopes what an agent can **create and deploy into**, not what it
143
+ can **touch once running**. That is narrower than earlier versions of this
144
+ document implied.
145
+
146
+ For a stronger guarantee today, use `EDGEGAP_READ_ONLY=1`, which never registers
147
+ the mutating tools at all, or point the agent at a separate non-production
148
+ organization. Both are unaffected by this gap.
149
+
150
+ Reported by Syed Anas Mohiuddin, September 2026.
86
151
 
87
152
  ### Environment variables
88
153
 
154
+ These configure the local server. On the remote endpoint they are set by
155
+ Edgegap and cannot be changed per developer — if you need any of them, run
156
+ locally.
157
+
89
158
  | Variable | Default | Purpose |
90
159
  | --- | --- | --- |
91
160
  | `EDGEGAP_API_TOKEN` | *(prompted)* | API token. Optional — omit it and the developer is asked at first use. The `token ` prefix is added for you. |
92
- | `EDGEGAP_READ_ONLY` | `0` | Set to `1` and the five mutating tools are never registered. The agent cannot see them, so it cannot be talked into calling them. |
93
- | `EDGEGAP_APP_ALLOWLIST` | *(empty)* | Comma-separated application names. When set, every tool refuses to touch anything else. |
161
+ | `EDGEGAP_READ_ONLY` | `0` | Set to `1` and the eight mutating tools (●, below) are never registered. The agent cannot see them, so it cannot be talked into calling them. |
162
+ | `EDGEGAP_APP_ALLOWLIST` | *(empty)* | Comma-separated application names. When set, the five application-keyed tools refuse to touch anything else. Does **not** scope the five `request_id`-keyed tools — see [Scope of the allowlist](#scope-of-the-allowlist). |
94
163
  | `EDGEGAP_MAX_DURATION_MINUTES` | `60` | Ceiling on `max_duration` the agent may set on a version. Caps runaway cost from an unattended agent. |
95
164
  | `EDGEGAP_TIMEOUT_MS` | `30000` | Per-request HTTP timeout. |
96
165
 
97
- ## The ten tools
166
+ ## Tools
167
+
168
+ Eighteen tools, grouped by where they fall on the path. The same set in both
169
+ modes; `EDGEGAP_READ_ONLY=1` hides the ● ones.
98
170
 
99
- Ordered along the golden path.
171
+ **Before the first deploy** — getting a headless build into a correct image and
172
+ into a registry, which is where agent-driven onboarding actually stalls.
100
173
 
101
- | # | Tool | Mutating | What it's for |
102
- | --- | --- | --- | --- |
103
- | 1 | `edgegap_list_apps` | | Orient before doing anything. Prevents duplicate applications. |
104
- | 2 | `edgegap_create_app` | ● | Create the container for versions. |
105
- | 3 | `edgegap_list_app_versions` | | Find a deployable version, or copy settings from a working one. |
106
- | 4 | `edgegap_create_app_version` | ● | Register a container image with CPU, memory, and ports. |
107
- | 5 | `edgegap_deploy` | ● | Start one instance near specified players. |
108
- | 6 | `edgegap_get_deployment` | | Single status read. |
109
- | 7 | `edgegap_wait_for_deployment` | | Poll to ready with backoff, then return the connection address. |
110
- | 8 | `edgegap_list_deployments` | | Find orphaned servers from earlier sessions. |
111
- | 9 | `edgegap_stop_deployment` | ● | Graceful SIGTERM, one deployment at a time. |
112
- | 10 | `edgegap_get_deployment_logs` | | Container output and crash exit code after a failure. |
174
+ | Tool | Mutating | What it's for |
175
+ | --- | --- | --- |
176
+ | `edgegap_validate_server_config` | | Static check of a Dockerfile, ports, resources and tag against Edgegap's requirements: linux/amd64, Unreal not running as root, Unity `-batchmode -nographics`, loopback binds, EXPOSE vs. version ports, protocol vs. netcode transport, `latest` tags. Returns Edgegap's reference Unity/Unreal Dockerfile when there is none yet or it fails. No API call. |
177
+ | `edgegap_get_registry_credentials` | ● | Push credentials for the org's private `registry.edgegap.com` project, plus the exact login/build/push commands and the values to pass to `edgegap_create_app_version`. Provisions the project on first use. |
178
+ | `edgegap_list_registry_tags` | | Confirm a pushed tag landed before registering it. |
179
+
180
+ **Dedicated servers** — the original golden path.
181
+
182
+ | Tool | Mutating | What it's for |
183
+ | --- | --- | --- |
184
+ | `edgegap_list_apps` | | Orient before doing anything. Prevents duplicate applications. |
185
+ | `edgegap_create_app` | ● | Create the container for versions. |
186
+ | `edgegap_list_app_versions` | | Find a deployable version, or copy settings from a working one. |
187
+ | `edgegap_create_app_version` | ● | Register a container image with CPU, memory, and ports. |
188
+ | `edgegap_deploy` | ● | Start one instance near specified players. |
189
+ | `edgegap_get_deployment` | | Single status read. |
190
+ | `edgegap_wait_for_deployment` | | Poll to ready with backoff, then return the connection address. |
191
+ | `edgegap_list_deployments` | | Find orphaned servers from earlier sessions. |
192
+ | `edgegap_stop_deployment` | ● | Graceful SIGTERM, one deployment at a time. |
193
+ | `edgegap_get_deployment_logs` | | Container output and crash exit code after a failure. |
194
+
195
+ **Peer-to-peer relays** — for co-op and host-client games, which need no server
196
+ image at all.
197
+
198
+ | Tool | Mutating | What it's for |
199
+ | --- | --- | --- |
200
+ | `edgegap_create_relay_session` | ● | Open a relay session for a set of player IPs, wait until it is ready, and return the relay address, ports, and per-player authorization tokens. |
201
+ | `edgegap_get_relay_session` | | Re-read a session. |
202
+ | `edgegap_authorize_relay_user` | ● | Add a player who joins after the session was created. |
203
+ | `edgegap_delete_relay_session` | ● | Close a session. |
204
+
205
+ **Matchmaking**
206
+
207
+ | Tool | Mutating | What it's for |
208
+ | --- | --- | --- |
209
+ | `edgegap_build_matchmaker_config` | | Generate a basic matchmaker configuration (teams, team size, optional latency rule and expansions) checked against the application version it deploys. Edgegap has no API for creating a matchmaker, so the developer uploads the result in the dashboard. |
113
210
 
114
211
  ## Design decisions
115
212
 
116
213
  **Curated, not generated.** The Edgegap API has roughly sixty operations.
117
214
  Auto-generating one tool per operation puts all sixty descriptions into the
118
- agent's context on every turn and measurably degrades tool selection. These ten
119
- cover the path that converts a new developer.
215
+ agent's context on every turn and measurably degrades tool selection. These
216
+ cover the path that converts a new developer — including the steps before the
217
+ first deploy, which are where that path used to end.
120
218
 
121
219
  **`wait_for_deployment` is a tool, not a loop.** Left to itself an agent will
122
220
  call a status endpoint in a tight loop, burn turns, and give up early. Folding
@@ -130,23 +228,47 @@ round trip to the human.
130
228
 
131
229
  **Local validation before the wire.** The memory-to-CPU ratio and the missing
132
230
  player location are caught here rather than surfacing as an opaque 400.
231
+ `edgegap_validate_server_config` extends this to the image itself, before a
232
+ build and push are spent discovering a problem.
233
+
234
+ **The registry token is handed to the agent; the API token never is.** The agent
235
+ has to run `docker login`, so the registry token is returned in the tool result.
236
+ It is scoped to the org's registry project, and the returned command reads it
237
+ from an environment variable over `--password-stdin` so it stays off command
238
+ lines and out of shell history. The tool is hidden in read-only mode.
239
+
240
+ **The registry credentials endpoint is not in the public spec.** It is
241
+ `GET /v1/wizard/registry-credentials`, the same call the Unity plugin makes,
242
+ preceded by `POST /v1/wizard/init-quick-start` when the project is not yet
243
+ provisioned.
133
244
 
134
245
  **Bulk operations are deliberately absent.** `stop` takes one `request_id`.
135
246
  There is no bulk-stop tool, because an agent with a filter expression and a bug
136
247
  can stop a production fleet.
137
248
 
249
+ **Both a hosted endpoint and a local package.** The hosted endpoint removes
250
+ every step between finding this server and calling a tool, which is where most
251
+ developers drop out. The local package is the only way to run the server
252
+ without extending custody of an unscoped token to a third party, including us.
253
+ Neither one dominates the other, so both ship. See `worker/DECISION.md` for the
254
+ longer version.
255
+
138
256
  ## Scope
139
257
 
140
- Not exposed, on purpose: matchmaking, relays, private fleets, smart fleets,
141
- endpoint storage, ACL/whitelist entries, deployment tags, metrics, container
142
- registry management, DNS configuration.
258
+ Not exposed, on purpose: private fleets, smart fleets, endpoint storage,
259
+ ACL/whitelist entries, deployment tags, metrics, registry tag deletion, DNS
260
+ configuration, and matchmaker lifecycle (start, stop, delete).
143
261
 
144
- These are real capabilities, but they belong to studios already operating on
145
- the platform, not to a developer deploying their first server. Adding them
146
- would trade the conversion path for surface area.
262
+ These belong to studios already operating on the platform, not to a developer
263
+ getting a first game online. Relays, registry push, and a basic matchmaker
264
+ config were moved in scope because agents hit them before the first deploy,
265
+ not after.
147
266
 
148
267
  ## Known limitation: asking for the token at all
149
268
 
269
+ This applies to local mode, where the token is collected through elicitation
270
+ rather than read from config.
271
+
150
272
  The MCP specification says servers should not use elicitation to collect
151
273
  sensitive data, and an API token is sensitive. This server does it anyway,
152
274
  because requiring a token in a config file before anything works is the largest
@@ -159,9 +281,10 @@ plain-language disclosure, required acknowledgement, redaction from all output,
159
281
  and the environment variable always winning when present. Removing any of them
160
282
  breaks the trade.
161
283
 
162
- The real fix is on Edgegap's side: scoped, revocable, deploy-only credentials,
163
- issued through OAuth rather than pasted as a secret. Until those exist, the
164
- interactive prompt is a workaround and is labelled as one in the code.
284
+ The real fix is on Edgegap's side and would improve both modes: scoped,
285
+ revocable, deploy-only credentials, issued through OAuth rather than pasted as
286
+ a secret. Until those exist, the interactive prompt is a workaround and is
287
+ labelled as one in the code.
165
288
 
166
289
  ## Development
167
290
 
@@ -170,9 +293,11 @@ npm run typecheck
170
293
  node smoke.mjs # handshake, tool registration, read-only mode
171
294
  node guards.mjs # local validation and allowlist enforcement
172
295
  node elicit.mjs # token prompt: accept, refuse acknowledgement, decline, no support
296
+ node newtools.mjs # validator, registry, relay, matchmaker tools against a local mock API
173
297
  ```
174
298
 
175
- None of these make network calls. `elicit.mjs` asserts that the prompt states
299
+ None of these reach Edgegap. `newtools.mjs` points `EDGEGAP_BASE_URL` at a
300
+ mock server on localhost. `elicit.mjs` asserts that the prompt states
176
301
  the org-wide scope, that the acknowledgement is required, that the token never
177
302
  appears in tool output, and that declining produces a stop-and-report message
178
303
  rather than a retry loop.
package/dist/auth.js CHANGED
@@ -189,24 +189,30 @@ export class TokenProvider {
189
189
  return token;
190
190
  }
191
191
  }
192
- /**
193
- * Credential source for the hosted Worker: the token arrives on the request
194
- * and lives only as long as this object, which is created per request and
195
- * garbage collected with it. Nothing is cached across requests, deliberately.
196
- */
197
192
  export class StaticTokenProvider {
198
193
  token;
194
+ unavailableMessage;
199
195
  used = false;
200
- constructor(token) {
196
+ /**
197
+ * @param token credential for this request, if the caller found a usable one
198
+ * @param unavailableMessage what to tell the agent when it calls a tool and
199
+ * there is no token. The transport knows WHY the token is missing — absent
200
+ * header, or a header carrying some other client's credential — and that
201
+ * distinction is the whole difference between a developer who can fix their
202
+ * setup and one staring at a generic 401. Defaults to the generic text.
203
+ */
204
+ constructor(token, unavailableMessage) {
201
205
  this.token = token;
206
+ this.unavailableMessage = unavailableMessage;
202
207
  }
203
208
  get current() {
204
209
  return this.token;
205
210
  }
206
211
  async get() {
207
212
  if (!this.token) {
208
- throw new TokenUnavailableError('No Edgegap API token on this request. Send it as an Authorization ' +
209
- 'header on the MCP connection.');
213
+ throw new TokenUnavailableError(this.unavailableMessage ??
214
+ 'No Edgegap API token on this request. Send it as an Authorization ' +
215
+ 'header on the MCP connection.');
210
216
  }
211
217
  return this.token;
212
218
  }
package/dist/client.js CHANGED
@@ -112,4 +112,35 @@ export class EdgegapClient {
112
112
  getDeploymentLogs(requestId, format = 'text') {
113
113
  return this.request('GET', 'v1', `/v1/deployment/${encodeURIComponent(requestId)}/container-logs`, { query: { format } });
114
114
  }
115
+ // --- Container registry -------------------------------------------------
116
+ /**
117
+ * Push credentials for the organization's project on registry.edgegap.com.
118
+ * Not in the published OpenAPI spec: this is the endpoint the Unity plugin
119
+ * uses, and it can fail until init-quick-start has provisioned the project.
120
+ */
121
+ getRegistryCredentials() {
122
+ return this.request('GET', 'v1', '/v1/wizard/registry-credentials');
123
+ }
124
+ /** Provisions the registry project if needed. Idempotent; returns 204. */
125
+ initQuickStart(source) {
126
+ return this.request('POST', 'v1', '/v1/wizard/init-quick-start', { body: { source } });
127
+ }
128
+ /** imageName is "<project>/<image>"; the slash is part of the route. */
129
+ listRegistryTags(imageName, query = {}) {
130
+ const path = imageName.split('/').map(encodeURIComponent).join('/');
131
+ return this.request('GET', 'v1', `/v1/container-registry/images/${path}/tags`, { query });
132
+ }
133
+ // --- Relays -------------------------------------------------------------
134
+ createRelaySession(body) {
135
+ return this.request('POST', 'v1', '/v1/relays/sessions', { body });
136
+ }
137
+ getRelaySession(sessionId) {
138
+ return this.request('GET', 'v1', `/v1/relays/sessions/${encodeURIComponent(sessionId)}`);
139
+ }
140
+ authorizeRelayUser(body) {
141
+ return this.request('POST', 'v1', '/v1/relays/sessions:authorize-user', { body });
142
+ }
143
+ deleteRelaySession(sessionId) {
144
+ return this.request('DELETE', 'v1', `/v1/relays/sessions/${encodeURIComponent(sessionId)}`);
145
+ }
115
146
  }
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * Edgegap MCP server.
4
4
  *
5
- * Exposes ten curated tools covering the path from "I have a game server
6
- * container" to "players are connected to it", so a coding agent can take a
7
- * developer through it without the developer reading the API reference.
5
+ * Exposes curated tools covering the path from "I have a headless server
6
+ * build" to "players are connected to it" — Dockerfile checks, registry push,
7
+ * deploy, relays for peer-to-peer games, and a matchmaker config — so a coding
8
+ * agent can take a developer through it without reading the API reference.
8
9
  *
9
10
  * Transport is stdio, which is what Claude Code, Cursor, Codex and VS Code use
10
11
  * for locally configured servers.
@@ -28,18 +29,26 @@ async function main() {
28
29
  }
29
30
  throw err;
30
31
  }
31
- const server = new McpServer({ name: 'edgegap', version: '0.1.0' }, {
32
- instructions: 'Deploy and operate game servers on Edgegap.\n\n' +
33
- 'Golden path for a first deployment:\n' +
34
- '1. edgegap_list_apps to see what already exists\n' +
35
- '2. edgegap_create_app if no suitable application is there\n' +
36
- '3. edgegap_create_app_version to register the container image\n' +
37
- '4. edgegap_deploy to start an instance near the players\n' +
38
- '5. edgegap_wait_for_deployment to get the connection address\n' +
39
- '6. edgegap_stop_deployment when finished\n\n' +
40
- 'Deployments cost money while running. Tag test deployments and stop ' +
41
- 'them before ending the task. If a deployment errors, read the container ' +
42
- 'logs before redeploying.\n\n' +
32
+ const server = new McpServer({ name: 'edgegap', version: '0.2.0' }, {
33
+ instructions: 'Host multiplayer games on Edgegap: dedicated servers, or relays for peer-to-peer.\n\n' +
34
+ 'Pick the path first. Peer-to-peer and host-client games (common for co-op) need no ' +
35
+ "server image: call edgegap_create_relay_session with the players' public IPs and " +
36
+ 'configure the relay transport with what it returns. Dedicated-server games follow ' +
37
+ 'the golden path below.\n\n' +
38
+ 'Golden path for a first dedicated-server deployment:\n' +
39
+ '1. edgegap_validate_server_config on the Dockerfile and ports, before building\n' +
40
+ '2. edgegap_get_registry_credentials, then docker build --platform linux/amd64 and push\n' +
41
+ '3. edgegap_list_registry_tags to confirm the push landed\n' +
42
+ '4. edgegap_list_apps, then edgegap_create_app if no suitable application exists\n' +
43
+ '5. edgegap_create_app_version to register the image\n' +
44
+ '6. edgegap_deploy to start an instance near the players\n' +
45
+ '7. edgegap_wait_for_deployment to get the connection address\n' +
46
+ '8. edgegap_stop_deployment when finished\n' +
47
+ 'To match players into those servers, edgegap_build_matchmaker_config produces the ' +
48
+ 'config the developer uploads in the dashboard.\n\n' +
49
+ 'Deployments and relay sessions cost money while running. Tag test deployments, and ' +
50
+ 'stop deployments and delete relay sessions you created before ending the task. If a ' +
51
+ 'deployment errors, read the container logs before redeploying.\n\n' +
43
52
  'Credentials: if no token was configured, the first tool call asks the ' +
44
53
  'developer for one. That token is org-wide and cannot be scoped by ' +
45
54
  'Edgegap, so it authorises far more than any single task needs. Treat it ' +
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Builds and checks a basic Edgegap matchmaker configuration.
3
+ *
4
+ * Edgegap has no public API for creating a matchmaker: it is created in the
5
+ * dashboard by uploading a JSON configuration. What an agent can usefully do is
6
+ * produce that JSON correctly the first time, pointed at an application version
7
+ * that actually exists, so the developer's only step is the upload.
8
+ *
9
+ * Format reference: https://docs.edgegap.com/learn/matchmaking/matchmaker-in-depth
10
+ */
11
+ /** Config schema version the docs currently publish. Overridable per call. */
12
+ export const MATCHMAKER_CONFIG_VERSION = '3.3.2';
13
+ export const DASHBOARD_URL = 'https://app.edgegap.com';
14
+ const DURATION = /^\d+(ms|s|m|h)$/;
15
+ function seconds(d) {
16
+ const m = d.match(/^(\d+)(ms|s|m|h)$/);
17
+ if (!m)
18
+ return NaN;
19
+ const n = Number(m[1]);
20
+ return m[2] === 'ms' ? n / 1000 : m[2] === 's' ? n : m[2] === 'm' ? n * 60 : n * 3600;
21
+ }
22
+ export function buildMatchmakerConfig(input) {
23
+ const problems = [];
24
+ const cautions = [];
25
+ if (input.min_team_size > input.max_team_size) {
26
+ problems.push(`min_team_size (${input.min_team_size}) is greater than max_team_size (${input.max_team_size}).`);
27
+ }
28
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(input.profile_name)) {
29
+ problems.push(`profile_name "${input.profile_name}" should be letters, digits, "-" or "_". Game clients send it on every ticket.`);
30
+ }
31
+ const expiration = input.ticket_expiration ?? '5m';
32
+ const removal = input.ticket_removal ?? '1m';
33
+ for (const [field, value] of [['ticket_expiration', expiration], ['ticket_removal', removal]]) {
34
+ if (!DURATION.test(value))
35
+ problems.push(`${field} "${value}" is not a duration like "30s", "5m" or "1h".`);
36
+ }
37
+ if (seconds(expiration) < 60) {
38
+ cautions.push(`ticket_expiration ${expiration} is short. Tickets must outlive the queue wait plus server start-up, or players are dropped before the match is ready.`);
39
+ }
40
+ const rules = {
41
+ match_size: {
42
+ type: 'player_count',
43
+ attributes: {
44
+ team_count: input.team_count,
45
+ min_team_size: input.min_team_size,
46
+ max_team_size: input.max_team_size,
47
+ },
48
+ },
49
+ };
50
+ const useLatency = input.max_latency_ms !== undefined || input.latency_difference_ms !== undefined;
51
+ if (useLatency) {
52
+ rules.beacons = {
53
+ type: 'latencies',
54
+ attributes: {
55
+ difference: input.latency_difference_ms ?? 100,
56
+ max_latency: input.max_latency_ms ?? 200,
57
+ },
58
+ };
59
+ }
60
+ const expansions = {};
61
+ let lastAfter = 0;
62
+ for (const e of [...(input.expansions ?? [])].sort((a, b) => a.after_seconds - b.after_seconds)) {
63
+ const step = {};
64
+ if (e.min_team_size !== undefined) {
65
+ if (e.min_team_size > input.max_team_size) {
66
+ problems.push(`expansion at ${e.after_seconds}s sets min_team_size ${e.min_team_size} above max_team_size ${input.max_team_size}.`);
67
+ }
68
+ step.match_size = { min_team_size: e.min_team_size };
69
+ }
70
+ if (e.max_latency_ms !== undefined) {
71
+ if (!useLatency) {
72
+ problems.push(`expansion at ${e.after_seconds}s relaxes max_latency_ms, but no latency rule is configured. Set max_latency_ms on the profile too.`);
73
+ }
74
+ step.beacons = { max_latency: e.max_latency_ms };
75
+ }
76
+ if (Object.keys(step).length === 0)
77
+ continue;
78
+ if (e.after_seconds === lastAfter)
79
+ problems.push(`two expansions share after_seconds ${e.after_seconds}.`);
80
+ lastAfter = e.after_seconds;
81
+ expansions[String(e.after_seconds)] = step;
82
+ }
83
+ if (input.team_count * input.min_team_size === 1) {
84
+ cautions.push('A match of one player starts a server per ticket. Fine for testing, costly in production.');
85
+ }
86
+ const config = {
87
+ version: input.config_version ?? MATCHMAKER_CONFIG_VERSION,
88
+ inspect: input.inspect ?? true,
89
+ max_deployment_retry_count: 3,
90
+ profiles: {
91
+ [input.profile_name]: {
92
+ ticket_expiration_period: expiration,
93
+ ticket_removal_period: removal,
94
+ group_inactivity_removal_period: '5m',
95
+ application: { name: input.application, version: input.version },
96
+ rules: { initial: rules, expansions },
97
+ },
98
+ },
99
+ };
100
+ return { config, problems, cautions };
101
+ }