antics-mcp
Advanced tools
+96
-1
@@ -385,3 +385,72 @@ #!/usr/bin/env node | ||
| } | ||
| async function publishModelTool(client, args) { | ||
| const missing = ["name", "glb", "recipe", "licence", "kitVersion"].filter((k) => !args[k]); | ||
| if (missing.length || !Array.isArray(args.sources)) { | ||
| return { | ||
| text: `publish_model needs ${[...missing, ...Array.isArray(args.sources) ? [] : ["sources"]].join(", ")}. sources may be [] but must be PRESENT \u2014 an empty list is a deliberate claim that nothing was derived, and it is not something to guess on someone's behalf. Build the .glb with \`npx modelkit build\` and render the card with \`npx modelkit preview <name> --card\`.`, | ||
| data: { error: true } | ||
| }; | ||
| } | ||
| try { | ||
| const res = await fetch(`${client.baseUrl}/api/models`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json", "x-antics-client": "mcp" }, | ||
| body: JSON.stringify({ | ||
| name: args.name, | ||
| glb: args.glb, | ||
| recipe: args.recipe, | ||
| licence: args.licence, | ||
| sources: args.sources, | ||
| kitVersion: args.kitVersion, | ||
| ...args.summary ? { summary: args.summary } : {}, | ||
| ...args.image ? { image: args.image } : {} | ||
| }) | ||
| }); | ||
| const body = await res.json(); | ||
| if (!res.ok) { | ||
| return { | ||
| text: `Model rejected (${body.error ?? res.status})${body.fields ? `: ${body.fields.join(", ")}` : ""}. ${body.hint ?? ""}`.trim(), | ||
| data: body | ||
| }; | ||
| } | ||
| let demo = ""; | ||
| if (args.demoFiles && Object.keys(args.demoFiles).length && body.id) { | ||
| const d = await fetch(`${client.baseUrl}/api/models/${body.id}/demo`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json", "x-antics-client": "mcp" }, | ||
| body: JSON.stringify({ files: args.demoFiles }) | ||
| }); | ||
| demo = d.ok ? " An interactive demo is attached." : " The demo bundle was rejected; the model still published."; | ||
| } | ||
| return { | ||
| text: `Published for review: ${body.url}.${demo} It is NOT public yet \u2014 an operator approves it, and the page is not indexed until then. The URL is permanent: republish the same model with PUT /api/models/${body.id} rather than publishing again, or the link you share goes stale.`, | ||
| data: body | ||
| }; | ||
| } catch (e) { | ||
| return { text: `Could not publish (${e instanceof Error ? e.message : "network error"}).`, data: { error: true } }; | ||
| } | ||
| } | ||
| async function findModelsTool(client, args) { | ||
| const p = new URLSearchParams(); | ||
| if (args.q) p.set("q", args.q); | ||
| if (args.limit) p.set("limit", String(args.limit)); | ||
| if (args.cursor) p.set("cursor", args.cursor); | ||
| try { | ||
| const res = await fetch(`${client.baseUrl}/api/models?${p}`, { headers: { "x-antics-client": "mcp" } }); | ||
| const body = await res.json(); | ||
| const list = body.models ?? []; | ||
| if (!list.length) return { text: args.q ? `No models match "${args.q}".` : "No models published yet.", data: body }; | ||
| return { | ||
| text: list.map((m) => `${m.name} \u2014 ${m.tris.toLocaleString()} tris, ${m.licence} | ||
| ${m.url}`).join("\n") + ` | ||
| Fetch <url>/llms.txt for how to use one.` + (body.nextCursor ? ` | ||
| More: pass cursor="${body.nextCursor}".` : ""), | ||
| data: body | ||
| }; | ||
| } catch (e) { | ||
| return { text: `Could not search models (${e instanceof Error ? e.message : "network error"}).`, data: { error: true } }; | ||
| } | ||
| } | ||
| // src/server.ts | ||
@@ -406,3 +475,3 @@ var wrap = (r) => ({ | ||
| - Leaderboard: await room.submitScore(n) | ||
| 3D GAMES: do NOT assemble primitives at page load \u2014 that is what makes a game look like a pile of boxes, and it costs startup time on every join. Model at DEV TIME with \`npm i antics-modelkit three\`, write a models module, \`npx modelkit build\` to .glb (Khronos-validated, quantised, with collider footprints in manifest.json), and ship the .glb in the files map. get_docs has the full recipe including the importmap the runtime loader needs. | ||
| 3D MODELS: publish_model puts a modelkit asset on a permanent /m/<slug> page with its recipe, licence and downloads (review-gated); find_models searches them, including by part name. 3D GAMES: do NOT assemble primitives at page load \u2014 that is what makes a game look like a pile of boxes, and it costs startup time on every join. Model at DEV TIME with \`npm i antics-modelkit three\`, write a models module, \`npx modelkit build\` to .glb (Khronos-validated, quantised, with collider footprints in manifest.json), and ship the .glb in the files map. get_docs has the full recipe including the importmap the runtime loader needs. | ||
| After deploying under a project, call set_share_preview to brand how its room links unfurl \u2014 set the title (name), description, and image (an https URL or uploaded image bytes) \u2014 and to fill the game's public world page: write about (what the game is) and rules (how to play) for every game. | ||
@@ -429,2 +498,28 @@ VERIFY WITHOUT A BROWSER: after deploy_game, call verify_game with the returned hash \u2014 it runs the game headlessly on the server (screenshot + live state probes via readState paths like "state.score" / "player.self.x", fast-forward with per-tick numeric traces, scripted key/pointer input, console output). Iterate on the numbers, not on screenshots: readState/trace catch bugs a final frame hides. Two-player sync check: verify_game { players: 2 } \u2014 per-page inputs: [{...}, {...}] lets both pages act. | ||
| server.tool( | ||
| "publish_model", | ||
| "Publish a 3D MODEL built with antics-modelkit (not a game \u2014 a model joins no rooms). Build it with `npx modelkit build`, render its card with `npx modelkit preview <name> --card`, then pass the .glb and the recipe that generates it. Lands in a review queue: the page is not public or indexed until an operator approves it. The URL is permanent \u2014 to update an asset later use PUT /api/models/<id>, never publish it again, or the link goes stale.", | ||
| { | ||
| name: z.string().describe("What the model is, e.g. 'Vending machine'."), | ||
| glb: z.string().describe("The built .glb, base64. Triangle count and part names are read from the FILE, not from you."), | ||
| recipe: z.string().describe("The models module that generates it \u2014 published alongside under MIT so anyone can rebuild it with different arguments."), | ||
| licence: z.enum(["CC0-1.0", "CC-BY-4.0", "CC-BY-SA-4.0"]).describe("Licence for the ASSET. Irreversible once published; CC0 in particular cannot be walked back. CC-BY-4.0 unless the user says otherwise."), | ||
| sources: z.array(z.string()).describe("What the asset was DERIVED from (a base mesh, a scan, a reference pack). Pass [] only if it is genuinely original \u2014 an empty list is a claim, not a default, and a human cannot see derivation in a mesh."), | ||
| kitVersion: z.string().describe("The antics-modelkit version it was built with, so the recipe can be run again."), | ||
| summary: z.string().optional().describe("One line for the card and the unfurl."), | ||
| image: z.string().optional().describe("Share card, base64 PNG, exactly 1200x630 \u2014 `npx modelkit preview <name> --card` produces it."), | ||
| demoFiles: z.record(z.string()).optional().describe("Optional interactive demo: a path -> content map like deploy_game's `files`. Runs sandboxed on the model page; the facts, licences and downloads stay on the page either way.") | ||
| }, | ||
| async (args) => wrap(await publishModelTool(client, args)) | ||
| ); | ||
| server.tool( | ||
| "find_models", | ||
| "Search published 3D models by name, summary or PART name (e.g. 'coil_r1c2'). Returns URLs; fetch <url>/llms.txt for how to use one in a game. Paged \u2014 pass the returned cursor for more.", | ||
| { | ||
| q: z.string().optional().describe("Text to match. Omit for the most recent."), | ||
| limit: z.number().optional().describe("1-100, default 24."), | ||
| cursor: z.string().optional().describe("From a previous call's nextCursor.") | ||
| }, | ||
| async (args) => wrap(await findModelsTool(client, args)) | ||
| ); | ||
| server.tool( | ||
| "deploy_game", | ||
@@ -431,0 +526,0 @@ "Deploy a game and get a playable multiplayer URL (keyless if no projectId). Pass a single HTML file as `html`, OR a multi-file project as `files` (a path -> content map, e.g. index.html + game.js + styles.css; binary assets as data: URIs). Write the game against the antics SDK first \u2014 call get_docs if you haven't.", |
+1
-1
| { | ||
| "name": "antics-mcp", | ||
| "version": "0.2.4", | ||
| "version": "0.2.5", | ||
| "mcpName": "io.github.antics-gg/antics-mcp", | ||
@@ -5,0 +5,0 @@ "description": "Multiplayer for your game, in one prompt \u2014 an MCP server that lets an AI agent deploy a web game to a playable multiplayer URL (rooms, state sync, leaderboards).", |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
53259
12.7%681
16.61%5
150%