realtime-avatar-mcp 0.4.1 → 0.5.1
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 +17 -10
- package/dist/server.d.ts +1 -1
- package/dist/server.js +112 -12
- package/package.json +2 -2
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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
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
|
-
-
|
|
60
|
-
from
|
|
61
|
-
retired — because "queued: 1" alone reads like nothing else
|
|
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:
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
8
|
+
export declare const MCP_VERSION = "0.5.1";
|
|
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.
|
|
13
|
+
export const MCP_VERSION = "0.5.1";
|
|
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.
|
|
55
|
-
"
|
|
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
|
|
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.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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",
|
|
@@ -109,6 +116,15 @@ export function createServer(options) {
|
|
|
109
116
|
`total ${credits(total)}` +
|
|
110
117
|
(page.nextCursor ? "\n(more pages available)" : ""));
|
|
111
118
|
});
|
|
119
|
+
server.registerTool("list_clips", {
|
|
120
|
+
title: "List an avatar's clip library",
|
|
121
|
+
description: "The declared clip library: every non-retired clip with its render status, plus " +
|
|
122
|
+
"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
|
+
"re-rendering keeps serving its previous take.",
|
|
125
|
+
inputSchema: { avatarId: z.string() },
|
|
126
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
127
|
+
}, async ({ avatarId }) => text(JSON.stringify(await rta.listClips(avatarId), null, 2)));
|
|
112
128
|
if (!options.allowWrites)
|
|
113
129
|
return server;
|
|
114
130
|
// ── writes, opt-in only ────────────────────────────────────────────────────
|
|
@@ -153,9 +169,70 @@ export function createServer(options) {
|
|
|
153
169
|
"Relay this payload to the browser byte-for-byte — the client SDK validates it " +
|
|
154
170
|
"strictly and rejects a reshaped object.");
|
|
155
171
|
});
|
|
172
|
+
server.registerTool("set_clip_library", {
|
|
173
|
+
title: "Declare an avatar's clip library",
|
|
174
|
+
description: "Declare the avatar's FULL desired clip library — a declaration, not a delta: " +
|
|
175
|
+
"unchanged clips are kept, new or changed ones are queued to render, omitted ones " +
|
|
176
|
+
"are retired. The 202 is acceptance, not readiness — poll list_clips until no row " +
|
|
177
|
+
"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(12).describe("The COMPLETE library. Omitted clips are retired."),
|
|
192
|
+
expectedRevision: z.number().int().optional()
|
|
193
|
+
.describe("CAS: the revision you last read. Omit to declare unconditionally."),
|
|
194
|
+
},
|
|
195
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
196
|
+
}, async ({ avatarId, clips, expectedRevision }) => {
|
|
197
|
+
const update = await rta.setClipLibrary(avatarId, { clips, expectedRevision });
|
|
198
|
+
const bucket = (label, ids) => `${label} (${ids.length})${ids.length ? ` ${ids.join(", ")}` : ""}`;
|
|
199
|
+
return text(`revision ${update.revision}\n` +
|
|
200
|
+
[
|
|
201
|
+
bucket("kept — still serving", update.plan.kept),
|
|
202
|
+
bucket("queued — rendering now", update.plan.queued),
|
|
203
|
+
bucket("retired — no longer live", update.plan.retired),
|
|
204
|
+
].join("\n") +
|
|
205
|
+
(update.plan.queued.length
|
|
206
|
+
? "\n\nQueued clips render in the background — poll list_clips until no row is queued or generating."
|
|
207
|
+
: ""));
|
|
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
|
+
});
|
|
156
231
|
server.registerTool("sync_clips", {
|
|
157
232
|
title: "Sync an avatar's clips",
|
|
158
|
-
description: "
|
|
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 " +
|
|
159
236
|
"the serve path only LOADS that cache — so a clip you added but never synced does " +
|
|
160
237
|
"nothing at all on the next call, silently. Idempotent: call it after every clip " +
|
|
161
238
|
"change. Pass the complete set you want live; anything omitted is retired. This does " +
|
|
@@ -226,10 +303,33 @@ export function createServer(options) {
|
|
|
226
303
|
const asset = await rta.createRemoteAsset({ kind, remoteUrl });
|
|
227
304
|
return text(`Created ${asset.id} (${kind})\n${asset.url}`);
|
|
228
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
|
+
});
|
|
229
327
|
server.registerTool("create_avatar_from_video", {
|
|
230
|
-
title: "Create an avatar from a video",
|
|
231
|
-
description: "
|
|
232
|
-
"
|
|
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.",
|
|
233
333
|
inputSchema: {
|
|
234
334
|
displayName: z.string(),
|
|
235
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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.
|
|
58
|
+
"realtime-avatar": "0.5.1",
|
|
59
59
|
"zod": "^4.4.3"
|
|
60
60
|
}
|
|
61
61
|
}
|