realtime-avatar-mcp 0.9.0 → 0.10.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
@@ -41,7 +41,6 @@ With `REALTIME_AVATAR_ALLOW_WRITES=1`:
41
41
  | --- | --- | --- |
42
42
  | `set_loop` | Re-direct her resting loop from a new description | no |
43
43
  | `set_clip_library` | Declare an avatar's full clip set as JSON — the platform renders it | no |
44
- | `sync_clips` | **Deprecated.** The external-URL tier. Use `set_clip_library` | no |
45
44
  | `upload_asset` | Upload a file **from this machine's disk**, get a public URL | no |
46
45
  | `create_remote_asset` | Register a file already on the internet — no local copy | no |
47
46
  | `create_avatar_from_image` | Build an avatar from ONE still — the loop is generated | no |
@@ -54,7 +53,7 @@ The five tools above are **read-only**, and each carries `readOnlyHint` so a hos
54
53
  the annotation rather than on a name it has to recognise. Pointed at a production key, this
55
54
  server is no more dangerous than a dashboard you left open.
56
55
 
57
- `REALTIME_AVATAR_ALLOW_WRITES=1` adds the eight write tools. Only `start_call` costs credits,
56
+ `REALTIME_AVATAR_ALLOW_WRITES=1` enables the write tools. Only `start_call` costs credits,
58
57
  and it **refuses a `tic_live_` key outright** — an operator who armed writes against a test
59
58
  key and later swapped in a production one should not discover it by being billed. Two
60
59
  independent gates, because one is a single mistake away from being none.
@@ -64,12 +63,11 @@ Two things to understand before arming writes:
64
63
  - **`upload_asset` reads local disk.** It opens whatever absolute path the agent names, on the
65
64
  machine running the server. Relative paths are refused rather than resolved against
66
65
  whatever directory the host spawned it in.
67
- - **The clip tools retire by omission.** Both `set_clip_library` and `sync_clips` take the
68
- COMPLETE set you want live; anything missing from it stops being served. Each reports all
69
- three buckets — kept/queued/retired — because "queued: 1" alone reads like nothing else
70
- changed. Declare with `set_clip_library`: clips are JSON (`motionPrompt` or an uploaded
71
- `assetId`) and the platform renders and hosts them. `sync_clips` is the deprecated tier
72
- that takes URLs on your own storage.
66
+ - **The clip declaration retires by omission.** `set_clip_library` takes the COMPLETE set;
67
+ anything missing from it stops being served. Its plan reports kept/queued/retired clips.
68
+ Declare `source.motionPrompt` or `source.assetId`; the platform hosts and prepares media.
69
+ The avatar's stored source is the implicit rest state — never declared, so the id
70
+ `primary` is reserved.
73
71
 
74
72
  ## Why an MCP server at all
75
73
 
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.9.0";
8
+ export declare const MCP_VERSION = "0.10.0";
9
9
  /**
10
10
  * The Realtime Avatar MCP server.
11
11
  *
package/dist/server.js CHANGED
@@ -3,14 +3,15 @@ import { stat } from "node:fs/promises";
3
3
  import { basename, extname, isAbsolute, resolve } from "node:path";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { z } from "zod";
6
- import { RealtimeAvatar, isQueued } from "realtime-avatar";
6
+ import { RealtimeAvatar, isQueued, clipLibraryDeclarationSchema, } from "realtime-avatar";
7
7
  /**
8
8
  * Mirrors this package's `version`. It reaches the wire twice — as the MCP server's own
9
9
  * identity, and inside the `User-Agent` — so a stale value misattributes real traffic.
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.9.0";
13
+ export const MCP_VERSION = "0.10.0";
14
+ const clipLibraryInputSchema = clipLibraryDeclarationSchema.safeExtend({ avatarId: z.string() });
14
15
  const MICROS_PER_CREDIT = 1_000_000;
15
16
  /** Extension -> asset kind, so a caller does not have to restate what the filename says. */
16
17
  const KIND_BY_EXT = {
@@ -120,7 +121,7 @@ export function createServer(options) {
120
121
  title: "List an avatar's clip library",
121
122
  description: "The declared clip library: every non-retired clip with its render status, plus " +
122
123
  "the library revision (pass it to set_clip_library as expectedRevision), the pose " +
123
- "anchor and eligibility. status is the render JOB, not serveability — a clip " +
124
+ "anchor, default source, idle and event behavior, and eligibility. status is the render JOB, not serveability — a clip " +
124
125
  "re-rendering keeps serving its previous take.",
125
126
  inputSchema: { avatarId: z.string() },
126
127
  annotations: { readOnlyHint: true, openWorldHint: true },
@@ -175,26 +176,19 @@ export function createServer(options) {
175
176
  "unchanged clips are kept, new or changed ones are queued to render, omitted ones " +
176
177
  "are retired. The 202 is acceptance, not readiness — poll list_clips until no row " +
177
178
  "is queued or generating. Pass expectedRevision from list_clips so a concurrent " +
178
- "writer surfaces as a 409 instead of a lost update. Does not spend credits.",
179
- inputSchema: {
180
- avatarId: z.string(),
181
- clips: z.array(z.object({
182
- clipId: z.string().describe("Stable id you choose; same id + same source = kept"),
183
- role: z.enum(["idle", "listen", "gesture"]),
184
- whenHint: z.string().optional().describe("Briefed to the character, like an actor"),
185
- source: z.union([
186
- z.object({ motionPrompt: z.string() }),
187
- z.object({ assetId: z.string() }),
188
- ]).describe("motionPrompt renders motion; assetId uploads a clip that must start AND end on the rest pose"),
189
- durationSeconds: z.number().optional(),
190
- reroll: z.boolean().optional().describe("Set true to force a re-render of the same prompt"),
191
- })).max(20).describe("The COMPLETE library (≤20 clips, ≤8 idle, ≤2 listen). Omitted clips are retired."),
192
- expectedRevision: z.number().int().optional()
193
- .describe("CAS: the revision you last read. Omit to declare unconditionally."),
194
- },
179
+ "writer surfaces as a 409 instead of a lost update. clips maps stable ids to sources; " +
180
+ "'primary' is reserved — the avatar's stored source is the implicit rest state, " +
181
+ "weight 1, and the one clip that may repeat. idle.clips are variations on resting, " +
182
+ "drawn uniformly and never twice running; idle.weight says how often a variation " +
183
+ "plays instead of resting. on.userSpeechStarted plays one listening reaction per " +
184
+ "speech episode; actions are requestable by description, never automatic. Clip " +
185
+ "references are checked before sending. New or changed sources can incur generation " +
186
+ "or pose-validation charges; behavior-only edits reuse media. Do not submit " +
187
+ "speculative declarations.",
188
+ inputSchema: clipLibraryInputSchema,
195
189
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
196
- }, async ({ avatarId, clips, expectedRevision }) => {
197
- const update = await rta.setClipLibrary(avatarId, { clips, expectedRevision });
190
+ }, async ({ avatarId, ...declaration }) => {
191
+ const update = await rta.setClipLibrary(avatarId, declaration);
198
192
  const bucket = (label, ids) => `${label} (${ids.length})${ids.length ? ` ${ids.join(", ")}` : ""}`;
199
193
  return text(`revision ${update.revision}\n` +
200
194
  [
@@ -209,8 +203,8 @@ export function createServer(options) {
209
203
  server.registerTool("set_loop", {
210
204
  title: "Re-direct the resting loop",
211
205
  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. " +
206
+ "from a new one-sentence description. This replaces the avatar's stored source, " +
207
+ "which is the implicit rest state her declared idle variations are weighted against. " +
214
208
  "Accepted immediately (202) and rendered over minutes; she stays ready and keeps " +
215
209
  "serving her previous loop the whole time, and the clip library is untouched. Bills " +
216
210
  "one video generation per call, so do not send it speculatively.",
@@ -228,33 +222,6 @@ export function createServer(options) {
228
222
  `She is still playing the previous loop until the new one is ready:\n ${result.servingUrl ?? "—"}\n\n` +
229
223
  "Poll get_avatar; the swap publishes in one step. Her clips are unaffected.");
230
224
  });
231
- server.registerTool("sync_clips", {
232
- title: "Sync an avatar's clips",
233
- description: "DEPRECATED — this serves the sunsetting external-URL clip tier; declare the " +
234
- "library with set_clip_library instead. Clips are prepared once and cached by URL " +
235
- "hash, and " +
236
- "the serve path only LOADS that cache — so a clip you added but never synced does " +
237
- "nothing at all on the next call, silently. Idempotent: call it after every clip " +
238
- "change. Pass the complete set you want live; anything omitted is retired. This does " +
239
- "not spend credits.",
240
- inputSchema: {
241
- avatarId: z.string(),
242
- clipUrls: z.array(z.string().url()).max(64)
243
- .describe("The COMPLETE set that should be live. Omitted clips are retired."),
244
- },
245
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
246
- }, async ({ avatarId, clipUrls }) => {
247
- const result = await rta.syncClips(avatarId, clipUrls);
248
- const line = (label, urls) => urls.length ? `${label} (${urls.length})\n ${urls.join("\n ")}` : `${label} (0)`;
249
- return text([
250
- line("queued — preparing now", result.queued),
251
- line("ready — already cached", result.ready),
252
- line("retired — no longer live", result.retired),
253
- ].join("\n") +
254
- (result.queued.length
255
- ? "\n\nQueued clips are not usable until they finish preparing."
256
- : ""));
257
- });
258
225
  server.registerTool("upload_asset", {
259
226
  title: "Upload a local file",
260
227
  description: "Upload a file FROM THIS MACHINE'S DISK and get back a public URL usable as a clip " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtime-avatar-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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.9.0",
58
+ "realtime-avatar": "0.10.0",
59
59
  "zod": "^4.4.3"
60
60
  }
61
61
  }