realtime-avatar-mcp 0.5.0 → 0.5.2

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
@@ -29,24 +29,28 @@ Working on it in this repo? `npm run build`, then point `command` at `node` with
29
29
  | `get_avatar` | Full detail for one |
30
30
  | `credit_balance` | Balance, and what in-flight calls have reserved |
31
31
  | `list_sessions` | The itemised bill — when each session ran, how long, what it cost |
32
+ | `list_clips` | An avatar's clip library: every clip, its status, the revision and rest pose |
32
33
 
33
34
  With `REALTIME_AVATAR_ALLOW_WRITES=1`:
34
35
 
35
36
  | Tool | | Bills? |
36
37
  | --- | --- | --- |
37
- | `sync_clips` | Prepare an avatar's clip set. Idempotent | no |
38
+ | `set_loop` | Re-direct her resting loop from a new description | no |
39
+ | `set_clip_library` | Declare an avatar's full clip set as JSON — the platform renders it | no |
40
+ | `sync_clips` | **Deprecated.** The external-URL tier. Use `set_clip_library` | no |
38
41
  | `upload_asset` | Upload a file **from this machine's disk**, get a public URL | no |
39
42
  | `create_remote_asset` | Register a file already on the internet — no local copy | no |
40
- | `create_avatar_from_video` | Build an avatar from a looping video URL | no |
43
+ | `create_avatar_from_image` | Build an avatar from ONE still — the loop is generated | no |
44
+ | `create_avatar_from_video` | **Deprecated.** Closed to new callers (422). Use the image tool | no |
41
45
  | `start_call` | Mint a live session to verify an integration | **yes, per second** |
42
46
 
43
47
  ## Spending money is opt-in, and twice gated
44
48
 
45
- The four tools above are **read-only**, and each carries `readOnlyHint` so a host can gate on
49
+ The five tools above are **read-only**, and each carries `readOnlyHint` so a host can gate on
46
50
  the annotation rather than on a name it has to recognise. Pointed at a production key, this
47
51
  server is no more dangerous than a dashboard you left open.
48
52
 
49
- `REALTIME_AVATAR_ALLOW_WRITES=1` adds the five write tools. Only `start_call` costs credits,
53
+ `REALTIME_AVATAR_ALLOW_WRITES=1` adds the eight write tools. Only `start_call` costs credits,
50
54
  and it **refuses a `tic_live_` key outright** — an operator who armed writes against a test
51
55
  key and later swapped in a production one should not discover it by being billed. Two
52
56
  independent gates, because one is a single mistake away from being none.
@@ -56,16 +60,19 @@ Two things to understand before arming writes:
56
60
  - **`upload_asset` reads local disk.** It opens whatever absolute path the agent names, on the
57
61
  machine running the server. Relative paths are refused rather than resolved against
58
62
  whatever directory the host spawned it in.
59
- - **`sync_clips` retires by omission.** Pass the complete set you want live; anything missing
60
- from the list stops being served. The tool reports all three buckets — queued, ready,
61
- retired — because "queued: 1" alone reads like nothing else changed.
63
+ - **The clip tools retire by omission.** Both `set_clip_library` and `sync_clips` take the
64
+ COMPLETE set you want live; anything missing from it stops being served. Each reports all
65
+ three buckets — kept/queued/retired — because "queued: 1" alone reads like nothing else
66
+ changed. Declare with `set_clip_library`: clips are JSON (`motionPrompt` or an uploaded
67
+ `assetId`) and the platform renders and hosts them. `sync_clips` is the deprecated tier
68
+ that takes URLs on your own storage.
62
69
 
63
70
  ## Why an MCP server at all
64
71
 
65
72
  An agent that reads docs still guesses. `ava_…` ids cannot be inferred, and the failures that
66
- cost the most time here are invisible in a type signature: an image-sourced avatar reports
67
- `ready` and publishes a black video track; a reshaped connection payload is rejected by the
68
- browser client; a 429 on a call is the queue, not an error.
73
+ cost the most time here are invisible in a type signature: a reshaped connection payload is
74
+ rejected by the browser client; a 429 on a call is the queue, not an error; the resting loop
75
+ is the avatar's source rather than a library clip, so no declaration changes it.
69
76
 
70
77
  `list_avatars` marks which avatars are actually usable. The server's `instructions` carry the
71
78
  relay rule and the per-second billing model. That is context an agent cannot derive, delivered
package/dist/server.d.ts CHANGED
@@ -5,7 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  * `test/server.test.ts` asserts the two stay equal; the equivalent constant in
6
6
  * `realtime-avatar` had that guard and this one did not, which is how it drifted.
7
7
  */
8
- export declare const MCP_VERSION = "0.5.0";
8
+ export declare const MCP_VERSION = "0.5.2";
9
9
  /**
10
10
  * The Realtime Avatar MCP server.
11
11
  *
package/dist/server.js CHANGED
@@ -10,7 +10,7 @@ import { RealtimeAvatar, isQueued } from "realtime-avatar";
10
10
  * `test/server.test.ts` asserts the two stay equal; the equivalent constant in
11
11
  * `realtime-avatar` had that guard and this one did not, which is how it drifted.
12
12
  */
13
- export const MCP_VERSION = "0.5.0";
13
+ export const MCP_VERSION = "0.5.2";
14
14
  const MICROS_PER_CREDIT = 1_000_000;
15
15
  /** Extension -> asset kind, so a caller does not have to restate what the filename says. */
16
16
  const KIND_BY_EXT = {
@@ -51,19 +51,26 @@ export function createServer(options) {
51
51
  server.registerTool("list_avatars", {
52
52
  title: "List avatars",
53
53
  description: "Every avatar on this account, with the id you pass to startCall. Call this before " +
54
- "writing code — avatar ids cannot be guessed. `sourceKind: 'image'` avatars publish " +
55
- "a BLACK video track on a live call; prefer 'video'.",
54
+ "writing code — avatar ids cannot be guessed. Callable means status 'ready' AND " +
55
+ "idleVideoStatus 'ready': the loop is attached. An avatar reads sourceKind 'video' " +
56
+ "once that happens, including the ones built from a single image — that is the " +
57
+ "normal end state, not a warning.",
56
58
  inputSchema: {},
57
59
  annotations: { readOnlyHint: true, openWorldHint: true },
58
60
  }, async () => {
59
61
  const avatars = await rta.listAvatars();
60
62
  if (avatars.length === 0) {
61
- return text("No avatars yet. Create one from a video URL before starting a call.");
63
+ return text("No avatars yet. Create one from a single portrait image before starting a call.");
62
64
  }
63
- const rows = avatars.map((a) => `${a.id} ${a.status.padEnd(13)} ${a.sourceKind.padEnd(5)} ${a.displayName}`);
64
- const usable = avatars.filter((a) => a.status === "ready" && a.sourceKind === "video");
65
- return text(`${avatars.length} avatar(s). ${usable.length} ready + video-sourced (usable for a live call).\n\n` +
66
- `id status kind name\n${rows.join("\n")}`);
65
+ const rows = avatars.map((a) => `${a.id} ${a.status.padEnd(13)} ${a.idleVideoStatus.padEnd(10)} ${a.displayName}`);
66
+ // Usable = ready WITH A LOOP ATTACHED. The old test was `sourceKind === "video"`,
67
+ // which happened to work only because an image-sourced avatar flips to `video` once
68
+ // its loop lands — it was reading the consequence, not the fact. `idleVideoStatus` is
69
+ // the fact, and it also distinguishes the three ways an avatar can be un-callable:
70
+ // still rendering (`queued`/`generating`), never had one (`none`), gave up (`failed`).
71
+ const usable = avatars.filter((a) => a.status === "ready" && a.idleVideoStatus === "ready");
72
+ return text(`${avatars.length} avatar(s). ${usable.length} ready with a loop attached (usable for a live call).\n\n` +
73
+ `id status loop name\n${rows.join("\n")}`);
67
74
  });
68
75
  server.registerTool("get_avatar", {
69
76
  title: "Get one avatar",
@@ -199,6 +206,28 @@ export function createServer(options) {
199
206
  ? "\n\nQueued clips render in the background — poll list_clips until no row is queued or generating."
200
207
  : ""));
201
208
  });
209
+ server.registerTool("set_loop", {
210
+ title: "Re-direct the resting loop",
211
+ description: "Re-generate the RESTING LOOP — the video she plays when nothing else is happening — " +
212
+ "from a new one-sentence description. NOT a clip: a clip with role 'idle' is a " +
213
+ "variant spliced over the loop, and declaring one never changes what she rests in. " +
214
+ "Accepted immediately (202) and rendered over minutes; she stays ready and keeps " +
215
+ "serving her previous loop the whole time, and the clip library is untouched. Bills " +
216
+ "one video generation per call, so do not send it speculatively.",
217
+ inputSchema: {
218
+ avatarId: z.string(),
219
+ motionPrompt: z.string().max(1200)
220
+ .describe("How she should idle. Describe a CLOSED arc that ends where it began, or the " +
221
+ "loop snaps every time it wraps — e.g. 'leans in, listening, a slow blink, " +
222
+ "settles back'."),
223
+ },
224
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
225
+ }, async ({ avatarId, motionPrompt }) => {
226
+ const result = await rta.setLoop(avatarId, { motionPrompt });
227
+ return text(`Re-direct accepted (${result.loopStatus}).\n` +
228
+ `She is still playing the previous loop until the new one is ready:\n ${result.servingUrl ?? "—"}\n\n` +
229
+ "Poll get_avatar; the swap publishes in one step. Her clips are unaffected.");
230
+ });
202
231
  server.registerTool("sync_clips", {
203
232
  title: "Sync an avatar's clips",
204
233
  description: "DEPRECATED — this serves the sunsetting external-URL clip tier; declare the " +
@@ -274,10 +303,33 @@ export function createServer(options) {
274
303
  const asset = await rta.createRemoteAsset({ kind, remoteUrl });
275
304
  return text(`Created ${asset.id} (${kind})\n${asset.url}`);
276
305
  });
306
+ server.registerTool("create_avatar_from_image", {
307
+ title: "Create an avatar from one still image",
308
+ description: "Build an avatar from a SINGLE still image — the shortest path, and the one to reach " +
309
+ "for by default. The platform generates the resting loop she idles in and a starter " +
310
+ "motion library rendered against her rest pose; no footage and no clip URLs are " +
311
+ "involved. 'motionPrompt' directs the resting loop and is the ONLY chance to direct " +
312
+ "it — no endpoint re-generates a loop after creation. Returns while she is still " +
313
+ "'preprocessing'; poll get_avatar.",
314
+ inputSchema: {
315
+ displayName: z.string(),
316
+ imageUrl: z.string().url().describe("Publicly reachable still of the character, face in frame"),
317
+ motionPrompt: z.string().max(1200).optional()
318
+ .describe("Art direction for the generated resting loop. Describe a CLOSED arc that ends " +
319
+ "where it began, or the loop snaps every time it wraps — e.g. 'settles into " +
320
+ "frame, breathes gently, a slow blink'. Omit for the house default."),
321
+ },
322
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
323
+ }, async ({ displayName, imageUrl, motionPrompt }) => {
324
+ const avatar = await rta.createAvatarFromImage({ displayName, imageUrl, motionPrompt });
325
+ return text(`Created ${avatar.id} (${avatar.status}). Poll get_avatar until it is ready.`);
326
+ });
277
327
  server.registerTool("create_avatar_from_video", {
278
- title: "Create an avatar from a video",
279
- description: "Build an avatar from a looping video URL. Use a VIDEO source: an avatar built from " +
280
- "a still image reaches 'ready' and then publishes a black track on every call.",
328
+ title: "Create an avatar from a video (deprecated)",
329
+ description: "DEPRECATED AND CLOSED — this answers 422 unless the tenant was already creating " +
330
+ "from video. Use create_avatar_from_image instead: one still, and the platform " +
331
+ "renders the loop and the motion library from it. Do not reach for this tool " +
332
+ "because an image is inconvenient to obtain; it will simply fail.",
281
333
  inputSchema: {
282
334
  displayName: z.string(),
283
335
  videoUrl: z.string().url().describe("Publicly reachable mp4, opening and closing on the same rest pose"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtime-avatar-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "MCP server for Realtime Avatar — let a coding agent read your avatars, balance and bill directly.",
5
5
  "license": "MIT",
6
6
  "author": "The Influence Company",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@modelcontextprotocol/sdk": "^1.30.0",
58
- "realtime-avatar": "0.5.0",
58
+ "realtime-avatar": "0.5.2",
59
59
  "zod": "^4.4.3"
60
60
  }
61
61
  }