@realmkeep/mcp 0.0.0-stage → 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,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @realmkeep/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lets an AI assistant (Claude, Codex, Cursor) fill in a Realmkeep game listing for you.
4
+ It creates and edits a **draft**, images included. You open the link it gives you, check it
5
+ and press Submit, so the site's spam and review checks still apply.
6
+
7
+ 1. Create an account-wide API key at `/dashboard/api-keys`. Keys scoped to one listing
8
+ cannot create new listings.
9
+ 2. Add the server to your client.
10
+
11
+ Claude Code:
12
+
13
+ ```
14
+ claude mcp add realmkeep -e REALMKEEP_API_KEY=rk_your_key -- npx -y @realmkeep/mcp
15
+ ```
16
+
17
+ Claude Desktop, Cursor and others, in the client's MCP config:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "realmkeep": {
23
+ "command": "npx",
24
+ "args": ["-y", "@realmkeep/mcp"],
25
+ "env": { "REALMKEEP_API_KEY": "rk_your_key" }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Codex, in `~/.codex/config.toml`:
32
+
33
+ ```toml
34
+ [mcp_servers.realmkeep]
35
+ command = "npx"
36
+ args = ["-y", "@realmkeep/mcp"]
37
+ env = { REALMKEEP_API_KEY = "rk_your_key" }
38
+ ```
39
+
40
+ `REALMKEEP_URL` overrides the site address (default `https://realmkeep.gg`).
41
+
42
+ ## Tools
43
+
44
+ Every tool writes drafts only. None of them submits a listing: the person does that in the
45
+ dashboard, so the site's checks still apply.
46
+
47
+ | Tool | Parameters | What it does |
48
+ | --- | --- | --- |
49
+ | `list_taxonomy` | none | Lists the platforms, genres, themes, engines and player modes a listing can carry. |
50
+ | `list_listings` | none | Lists the account's listings with id, name and state. |
51
+ | `create_listing_draft` | `name` | Creates a draft from a game name and returns it with a `missing` list. |
52
+ | `get_listing` | `id` | Reads one listing: every field, its links and terms, and what is still missing. |
53
+ | `upload_listing_image` | `listingId`, `path`, `role` (`banner`, `logo` or `screenshot`), `altText` | Reads an image file from your computer and uploads it to a draft. A banner or logo replaces the existing one, screenshots are added. PNG, JPEG, WebP or AVIF up to 8 MB; `altText` is required. Refused once the listing is submitted. |
54
+ | `update_listing` | `id`, then any of `name`, `tagline`, `descriptionMarkdown`, `playUrl`, `contactEmail`, `launchStatus`, `nsfw`, `links`, `platforms`, `genres`, `themes`, `engines`, `playerModes` and the other listing fields | Fills in a subset of a draft. Send only what changes, `null` clears an optional field. Refused once the listing is submitted. |
55
+
56
+ The API behind them is documented at `/docs/api`, and described in `/openapi.json`.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createClient } from './client.js';
4
+ import { createServer } from './server.js';
5
+ const apiKey = process.env['REALMKEEP_API_KEY'];
6
+ if (apiKey === undefined || apiKey === '') {
7
+ console.error('Set REALMKEEP_API_KEY to an account-wide key from /dashboard/api-keys.');
8
+ process.exit(1);
9
+ }
10
+ const client = createClient({
11
+ apiKey,
12
+ baseUrl: process.env['REALMKEEP_URL'] ?? 'https://realmkeep.gg',
13
+ });
14
+ await createServer(client).connect(new StdioServerTransport());
15
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;AAC/C,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;IAC1C,OAAO,CAAC,KAAK,CAAC,wEAAwE,CAAC,CAAA;IACvF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC;AAED,MAAM,MAAM,GAAG,YAAY,CAAC;IAC1B,MAAM;IACN,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,sBAAsB;CAChE,CAAC,CAAA;AAEF,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A thin client for the Realmkeep listing API. The MCP server holds no listing logic:
3
+ * every rule (limits, content check, drafts only) lives on the site and comes back as
4
+ * an error message the model can read.
5
+ */
6
+ export interface ClientOptions {
7
+ apiKey: string;
8
+ baseUrl: string;
9
+ fetch?: typeof fetch;
10
+ }
11
+ export declare class RealmkeepError extends Error {
12
+ readonly status: number;
13
+ readonly code: string;
14
+ constructor(status: number, code: string, message: string);
15
+ }
16
+ export interface RealmkeepClient {
17
+ request(method: string, path: string, body?: unknown): Promise<unknown>;
18
+ /** POST a multipart form, for the one endpoint that takes a file. */
19
+ upload(path: string, form: FormData): Promise<unknown>;
20
+ }
21
+ export declare function createClient(options: ClientOptions): RealmkeepClient;
22
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAED,qBAAa,cAAe,SAAQ,KAAK;IAErC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACvE,qEAAqE;IACrE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACvD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,eAAe,CA2CpE"}
package/dist/client.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A thin client for the Realmkeep listing API. The MCP server holds no listing logic:
3
+ * every rule (limits, content check, drafts only) lives on the site and comes back as
4
+ * an error message the model can read.
5
+ */
6
+ export class RealmkeepError extends Error {
7
+ status;
8
+ code;
9
+ constructor(status, code, message) {
10
+ super(message);
11
+ this.status = status;
12
+ this.code = code;
13
+ this.name = 'RealmkeepError';
14
+ }
15
+ }
16
+ export function createClient(options) {
17
+ const doFetch = options.fetch ?? fetch;
18
+ const base = options.baseUrl.replace(/\/$/, '');
19
+ async function send(path, init, headers) {
20
+ const response = await doFetch(`${base}/api/v1${path}`, {
21
+ ...init,
22
+ headers: {
23
+ authorization: `Bearer ${options.apiKey}`,
24
+ accept: 'application/json',
25
+ ...headers,
26
+ },
27
+ });
28
+ const text = await response.text();
29
+ let json;
30
+ try {
31
+ json = text === '' ? null : JSON.parse(text);
32
+ }
33
+ catch {
34
+ json = null;
35
+ }
36
+ if (!response.ok) {
37
+ const error = json?.error;
38
+ throw new RealmkeepError(response.status, error?.code ?? 'http_error', error?.message ?? `Realmkeep answered ${response.status}`);
39
+ }
40
+ return json;
41
+ }
42
+ return {
43
+ request: (method, path, body) => send(path, { method, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }, { 'content-type': 'application/json' }),
44
+ // No content-type here: fetch sets it, with the boundary, from the FormData.
45
+ upload: (path, form) => send(path, { method: 'POST', body: form }, {}),
46
+ };
47
+ }
48
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAE5B;IACA;IAFX,YACW,MAAc,EACd,IAAY,EACrB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAA;QAJL,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAIrB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;IAC9B,CAAC;CACF;AAQD,MAAM,UAAU,YAAY,CAAC,OAAsB;IACjD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAA;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IAE/C,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,IAAiB,EAAE,OAA+B;QAClF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,UAAU,IAAI,EAAE,EAAE;YACtD,GAAG,IAAI;YACP,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,OAAO,CAAC,MAAM,EAAE;gBACzC,MAAM,EAAE,kBAAkB;gBAC1B,GAAG,OAAO;aACX;SACF,CAAC,CAAA;QAEF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAClC,IAAI,IAAa,CAAA;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,IAAI,CAAA;QACb,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,KAAK,GAAI,IAA+D,EAAE,KAAK,CAAA;YACrF,MAAM,IAAI,cAAc,CACtB,QAAQ,CAAC,MAAM,EACf,KAAK,EAAE,IAAI,IAAI,YAAY,EAC3B,KAAK,EAAE,OAAO,IAAI,sBAAsB,QAAQ,CAAC,MAAM,EAAE,CAC1D,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO;QACL,OAAO,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAC9B,IAAI,CACF,IAAI,EACJ,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EACzE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CACvC;QACH,6EAA6E;QAC7E,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;KACvE,CAAA;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { createClient, RealmkeepError } from './client.js';
2
+ export type { RealmkeepClient } from './client.js';
3
+ export { createServer } from './server.js';
4
+ export { tools } from './tools.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC1D,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { createClient, RealmkeepError } from './client.js';
2
+ export { createServer } from './server.js';
3
+ export { tools } from './tools.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE1D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type RealmkeepClient } from './client.js';
3
+ export declare function createServer(client: RealmkeepClient): McpServer;
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAGlE,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAmB/D"}
package/dist/server.js ADDED
@@ -0,0 +1,22 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { RealmkeepError } from './client.js';
3
+ import { tools } from './tools.js';
4
+ export function createServer(client) {
5
+ const server = new McpServer({ name: 'realmkeep', version: '0.2.0' });
6
+ for (const tool of tools) {
7
+ server.tool(tool.name, tool.description, tool.shape, async (args) => {
8
+ try {
9
+ const result = await tool.run(client, args);
10
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
11
+ }
12
+ catch (error) {
13
+ // The site's message is written to be read: it names the limit, the field, or
14
+ // the reason. Returned as a tool error so the model can correct and retry.
15
+ const message = error instanceof RealmkeepError ? `${error.code}: ${error.message}` : String(error);
16
+ return { isError: true, content: [{ type: 'text', text: message }] };
17
+ }
18
+ });
19
+ }
20
+ return server;
21
+ }
22
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACnE,OAAO,EAAE,cAAc,EAAwB,MAAM,aAAa,CAAA;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAA;IAErE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAClE,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAA+B,CAAC,CAAA;gBACtE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;YACxF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,8EAA8E;gBAC9E,2EAA2E;gBAC3E,MAAM,OAAO,GACX,KAAK,YAAY,cAAc,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACrF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAA;YAC/E,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import type { RealmkeepClient } from './client.js';
3
+ export declare const listingFields: {
4
+ readonly name: z.ZodString;
5
+ readonly tagline: z.ZodString;
6
+ readonly descriptionMarkdown: z.ZodString;
7
+ readonly playUrl: z.ZodString;
8
+ readonly contactEmail: z.ZodString;
9
+ readonly launchStatus: z.ZodString;
10
+ readonly nsfw: z.ZodBoolean;
11
+ readonly developerName: z.ZodNullable<z.ZodString>;
12
+ readonly publisherName: z.ZodNullable<z.ZodString>;
13
+ readonly releaseDate: z.ZodNullable<z.ZodString>;
14
+ readonly serverRegion: z.ZodNullable<z.ZodString>;
15
+ readonly languages: z.ZodArray<z.ZodString, "many">;
16
+ readonly monetization: z.ZodNullable<z.ZodString>;
17
+ readonly hasAds: z.ZodBoolean;
18
+ readonly hasIap: z.ZodBoolean;
19
+ readonly payToWin: z.ZodBoolean;
20
+ readonly keyboardOnly: z.ZodBoolean;
21
+ readonly remappableControls: z.ZodBoolean;
22
+ readonly colourblindFriendly: z.ZodBoolean;
23
+ readonly subtitled: z.ZodBoolean;
24
+ readonly noFlashing: z.ZodBoolean;
25
+ readonly ageRatingSystem: z.ZodNullable<z.ZodString>;
26
+ readonly ageRatingValue: z.ZodNullable<z.ZodString>;
27
+ readonly trailerUrl: z.ZodNullable<z.ZodString>;
28
+ readonly serverHost: z.ZodNullable<z.ZodString>;
29
+ readonly serverPort: z.ZodNullable<z.ZodNumber>;
30
+ readonly voteRewardText: z.ZodNullable<z.ZodString>;
31
+ readonly voteCallbackUrl: z.ZodNullable<z.ZodString>;
32
+ readonly redirectAfterVote: z.ZodBoolean;
33
+ readonly links: z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>;
34
+ readonly platforms: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
35
+ readonly genres: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
36
+ readonly themes: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
37
+ readonly engines: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
38
+ readonly playerModes: z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">;
39
+ };
40
+ export interface Tool {
41
+ name: string;
42
+ description: string;
43
+ shape: z.ZodRawShape;
44
+ run(client: RealmkeepClient, args: Record<string, unknown>): Promise<unknown>;
45
+ }
46
+ export declare const tools: readonly Tool[];
47
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAkBlD,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoChB,CAAA;AAEV,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,CAAC,CAAC,WAAW,CAAA;IACpB,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC9E;AAgBD,eAAO,MAAM,KAAK,EAAE,SAAS,IAAI,EAmFhC,CAAA"}
package/dist/tools.js ADDED
@@ -0,0 +1,136 @@
1
+ import { readFile, stat } from 'node:fs/promises';
2
+ import { basename, extname } from 'node:path';
3
+ import { z } from 'zod';
4
+ /**
5
+ * The tools an AI assistant gets. Each is a single API call and returns the site's own
6
+ * JSON, so what the model sees is what the API says, including the `missing` list and
7
+ * the `dashboardUrl` it should hand to the person.
8
+ */
9
+ const SOCIAL = z
10
+ .record(z.string(), z.string().nullable())
11
+ .describe('Network to URL, for example { "youtube": "https://youtube.com/@x" }. null removes one.');
12
+ const TERMS = z
13
+ .array(z.union([z.string(), z.number()]))
14
+ .describe('Names, slugs or ids from list_taxonomy. A name the site lacks becomes a suggestion.');
15
+ export const listingFields = {
16
+ name: z.string().describe('Game name'),
17
+ tagline: z.string().describe('One line, at least 10 characters'),
18
+ descriptionMarkdown: z.string().describe('Markdown, at least 150 characters of text'),
19
+ playUrl: z.string().describe('https URL where people play or download the game'),
20
+ contactEmail: z.string().describe('Email address for the site to reach the owner'),
21
+ launchStatus: z.string().describe('in_development, alpha, beta, early_access, live or sunset'),
22
+ nsfw: z.boolean().describe('True for adult content'),
23
+ developerName: z.string().nullable(),
24
+ publisherName: z.string().nullable(),
25
+ releaseDate: z.string().nullable().describe('YYYY-MM-DD'),
26
+ serverRegion: z.string().nullable(),
27
+ languages: z.array(z.string()).describe('Language codes'),
28
+ monetization: z.string().nullable(),
29
+ hasAds: z.boolean(),
30
+ hasIap: z.boolean(),
31
+ payToWin: z.boolean(),
32
+ keyboardOnly: z.boolean(),
33
+ remappableControls: z.boolean(),
34
+ colourblindFriendly: z.boolean(),
35
+ subtitled: z.boolean(),
36
+ noFlashing: z.boolean(),
37
+ ageRatingSystem: z.string().nullable().describe('pegi or esrb'),
38
+ ageRatingValue: z.string().nullable(),
39
+ trailerUrl: z.string().nullable().describe('YouTube link or direct .mp4 link'),
40
+ serverHost: z.string().nullable(),
41
+ serverPort: z.number().int().nullable(),
42
+ voteRewardText: z.string().nullable(),
43
+ voteCallbackUrl: z.string().nullable(),
44
+ redirectAfterVote: z.boolean(),
45
+ links: SOCIAL,
46
+ platforms: TERMS,
47
+ genres: TERMS,
48
+ themes: TERMS,
49
+ engines: TERMS.describe('At most one engine'),
50
+ playerModes: TERMS,
51
+ };
52
+ const HAND_OVER = 'You cannot submit a listing. When the draft is filled in, give the person the dashboardUrl so they can check it and press Submit.';
53
+ const IMAGE_TYPES = {
54
+ '.png': 'image/png',
55
+ '.jpg': 'image/jpeg',
56
+ '.jpeg': 'image/jpeg',
57
+ '.webp': 'image/webp',
58
+ '.avif': 'image/avif',
59
+ };
60
+ /** The size the site accepts, checked here so a huge file is not read into memory first. */
61
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
62
+ export const tools = [
63
+ {
64
+ name: 'list_taxonomy',
65
+ description: 'List the platforms, genres, themes, engines and player modes Realmkeep supports. Call this before choosing terms for a listing.',
66
+ shape: {},
67
+ run: (client) => client.request('GET', '/taxonomy'),
68
+ },
69
+ {
70
+ name: 'list_listings',
71
+ description: "List the account's listings with their id, name and state.",
72
+ shape: {},
73
+ run: (client) => client.request('GET', '/listings'),
74
+ },
75
+ {
76
+ name: 'create_listing_draft',
77
+ description: `Create a new draft listing from a game name. Returns the draft with a missing list. ${HAND_OVER}`,
78
+ shape: { name: listingFields.name },
79
+ run: (client, args) => client.request('POST', '/listings', { name: args['name'] }),
80
+ },
81
+ {
82
+ name: 'get_listing',
83
+ description: 'Read one listing: every field, its links and terms, and what is still missing before it can be submitted.',
84
+ shape: { id: z.string().describe('Listing id') },
85
+ run: (client, args) => client.request('GET', `/listings/${encodeURIComponent(String(args['id']))}`),
86
+ },
87
+ {
88
+ name: 'upload_listing_image',
89
+ description: 'Upload an image from a file on this computer to a draft listing. role is banner (wide, at least 1600 px across), logo (square, at least 512 px) or screenshot (at least 1600 px across). A banner or logo replaces the existing one; screenshots are added. altText is required: describe what the image shows. PNG, JPEG, WebP or AVIF up to 8 MB. Only drafts accept uploads.',
90
+ shape: {
91
+ listingId: z.string().describe('Listing id'),
92
+ path: z.string().describe('Path of the image file on the local machine'),
93
+ role: z.enum(['banner', 'logo', 'screenshot']),
94
+ altText: z.string().describe('What the image shows, up to 300 characters'),
95
+ },
96
+ run: async (client, args) => {
97
+ const path = String(args['path']);
98
+ const type = IMAGE_TYPES[extname(path).toLowerCase()];
99
+ if (type === undefined) {
100
+ throw new Error('The file must be a .png, .jpg, .jpeg, .webp or .avif image');
101
+ }
102
+ const info = await stat(path);
103
+ if (!info.isFile()) {
104
+ throw new Error('The path must be a regular image file');
105
+ }
106
+ if (info.size > MAX_IMAGE_BYTES) {
107
+ throw new Error('The file is larger than 8 MB. Save it at a lower quality and try again.');
108
+ }
109
+ const bytes = await readFile(path);
110
+ if (bytes.byteLength > MAX_IMAGE_BYTES) {
111
+ throw new Error('The file is larger than 8 MB. Save it at a lower quality and try again.');
112
+ }
113
+ const form = new FormData();
114
+ form.set('file', new File([new Uint8Array(bytes)], basename(path), { type }));
115
+ form.set('role', String(args['role']));
116
+ form.set('altText', String(args['altText']));
117
+ return client.upload(`/listings/${encodeURIComponent(String(args['listingId']))}/media`, form);
118
+ },
119
+ },
120
+ {
121
+ name: 'update_listing',
122
+ description: `Fill in any subset of a draft listing. Send only the fields you want to change; null clears an optional field. Only drafts can be changed. ${HAND_OVER}`,
123
+ shape: {
124
+ id: z.string().describe('Listing id'),
125
+ ...Object.fromEntries(Object.entries(listingFields)
126
+ .filter(([key]) => key !== 'name')
127
+ .map(([key, schema]) => [key, schema.optional()])),
128
+ name: listingFields.name.optional(),
129
+ },
130
+ run: (client, args) => {
131
+ const { id, ...fields } = args;
132
+ return client.request('PATCH', `/listings/${encodeURIComponent(String(id))}`, Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined)));
133
+ },
134
+ },
135
+ ];
136
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@realmkeep/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "mcpName": "io.github.mortek/realmkeep",
4
+ "version": "0.2.0",
5
+ "description": "MCP server that lets an AI assistant fill in a Realmkeep game listing as a draft",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "realmkeep-mcp": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "!dist/.tsbuildinfo"
15
+ ],
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.32.2",
18
+ "zod": "^3.24.1"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "license": "UNLICENSED",
24
+ "repository": {
25
+ "type": "git",
26
+ "directory": "packages/mcp"
27
+ }
28
+ }