@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 +183 -58
- package/dist/auth.js +14 -8
- package/dist/client.js +31 -0
- package/dist/index.js +24 -15
- package/dist/matchmaker.js +101 -0
- package/dist/serverconfig.js +299 -0
- package/dist/tools.js +388 -4
- package/package.json +3 -3
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
|
|
4
|
-
server
|
|
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
|
-
|
|
8
|
-
[
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
##
|
|
60
|
+
## Where your token goes
|
|
32
61
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
48
|
-
clients that cannot show prompts. Do not pass a token as a
|
|
49
|
-
argument — arguments are visible to other processes via `ps`, and
|
|
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
|
-
**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
93
|
-
| `EDGEGAP_APP_ALLOWLIST` | *(empty)* | Comma-separated application names. When set,
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
|
102
|
-
| --- | --- | --- |
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
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
|
|
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:
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
163
|
-
issued through OAuth rather than pasted as
|
|
164
|
-
interactive prompt is a workaround and is
|
|
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
|
|
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
|
-
|
|
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(
|
|
209
|
-
'
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
32
|
-
instructions: '
|
|
33
|
-
'
|
|
34
|
-
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
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
|
+
}
|