realtime-avatar-mcp 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Influence Company
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # realtime-avatar-mcp
2
+
3
+ **Not published yet** — the package is marked `private: true`, so `npm publish` skips it rather than relying on nobody running the command. Build it and point your agent at the local path.
4
+
5
+ An MCP server for Realtime Avatar. [`AGENTS.md`](../../AGENTS.md) tells a coding agent what
6
+ the API is; this lets it *look* — at your avatars, your balance, your bill — instead of
7
+ guessing ids and inventing shapes.
8
+
9
+ ```bash
10
+ npm run build
11
+ ```
12
+
13
+ ```jsonc
14
+ {
15
+ "mcpServers": {
16
+ "realtime-avatar": {
17
+ "command": "node",
18
+ "args": ["/abs/path/to/realtime-avatar-sdk/libs/mcp/dist/bin.js"],
19
+ "env": { "REALTIME_AVATAR_API_KEY": "tic_test_…" }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ## Tools
26
+
27
+ | Tool | |
28
+ | --- | --- |
29
+ | `list_avatars` | Every avatar with the id you pass to `startCall`, and which are usable live |
30
+ | `get_avatar` | Full detail for one |
31
+ | `credit_balance` | Balance, and what in-flight calls have reserved |
32
+ | `list_sessions` | The itemised bill — when each session ran, how long, what it cost |
33
+
34
+ With `REALTIME_AVATAR_ALLOW_WRITES=1`:
35
+
36
+ | Tool | | Bills? |
37
+ | --- | --- | --- |
38
+ | `sync_clips` | Prepare an avatar's clip set. Idempotent | no |
39
+ | `upload_asset` | Upload a file **from this machine's disk**, get a public URL | no |
40
+ | `create_remote_asset` | Register a file already on the internet — no local copy | no |
41
+ | `create_avatar_from_video` | Build an avatar from a looping video URL | no |
42
+ | `start_call` | Mint a live session to verify an integration | **yes, per second** |
43
+
44
+ ## Spending money is opt-in, and twice gated
45
+
46
+ The four tools above are **read-only**, and each carries `readOnlyHint` so a host can gate on
47
+ the annotation rather than on a name it has to recognise. Pointed at a production key, this
48
+ server is no more dangerous than a dashboard you left open.
49
+
50
+ `REALTIME_AVATAR_ALLOW_WRITES=1` adds the five write tools. Only `start_call` costs credits,
51
+ and it **refuses a `tic_live_` key outright** — an operator who armed writes against a test
52
+ key and later swapped in a production one should not discover it by being billed. Two
53
+ independent gates, because one is a single mistake away from being none.
54
+
55
+ Two things to understand before arming writes:
56
+
57
+ - **`upload_asset` reads local disk.** It opens whatever absolute path the agent names, on the
58
+ machine running the server. Relative paths are refused rather than resolved against
59
+ whatever directory the host spawned it in.
60
+ - **`sync_clips` retires by omission.** Pass the complete set you want live; anything missing
61
+ from the list stops being served. The tool reports all three buckets — queued, ready,
62
+ retired — because "queued: 1" alone reads like nothing else changed.
63
+
64
+ ## Why an MCP server at all
65
+
66
+ An agent that reads docs still guesses. `ava_…` ids cannot be inferred, and the failures that
67
+ cost the most time here are invisible in a type signature: an image-sourced avatar reports
68
+ `ready` and publishes a black video track; a reshaped connection payload is rejected by the
69
+ browser client; a 429 on a call is the queue, not an error.
70
+
71
+ `list_avatars` marks which avatars are actually usable. The server's `instructions` carry the
72
+ relay rule and the per-second billing model. That is context an agent cannot derive, delivered
73
+ where it is about to act.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * stdio entry point. Configure it in your agent host:
4
+ *
5
+ * {
6
+ * "mcpServers": {
7
+ * "realtime-avatar": {
8
+ * "command": "npx",
9
+ * "args": ["-y", "realtime-avatar-mcp"],
10
+ * "env": { "REALTIME_AVATAR_API_KEY": "tic_test_…" }
11
+ * }
12
+ * }
13
+ * }
14
+ */
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { createServer } from "./server.js";
17
+ const apiKey = process.env.REALTIME_AVATAR_API_KEY;
18
+ if (!apiKey) {
19
+ // stderr, never stdout: stdout IS the protocol channel and a stray line corrupts it.
20
+ console.error("REALTIME_AVATAR_API_KEY is not set. The server needs a tic_test_ or tic_live_ key.");
21
+ process.exit(1);
22
+ }
23
+ const server = createServer({
24
+ apiKey,
25
+ baseUrl: process.env.REALTIME_AVATAR_BASE_URL,
26
+ // Off unless asked for. The default surface cannot spend credits.
27
+ allowWrites: process.env.REALTIME_AVATAR_ALLOW_WRITES === "1",
28
+ });
29
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1 @@
1
+ export { createServer, type CreateServerOptions } from "./server.ts";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createServer } from "./server.js";
@@ -0,0 +1,40 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Mirrors this package's `version`. It reaches the wire twice — as the MCP server's own
4
+ * identity, and inside the `User-Agent` — so a stale value misattributes real traffic.
5
+ * `test/server.test.ts` asserts the two stay equal; the equivalent constant in
6
+ * `realtime-avatar` had that guard and this one did not, which is how it drifted.
7
+ */
8
+ export declare const MCP_VERSION = "0.3.0";
9
+ /**
10
+ * The Realtime Avatar MCP server.
11
+ *
12
+ * `AGENTS.md` tells an agent what the API is. This lets it *look* — at your avatars, your
13
+ * balance, your bill — instead of guessing ids and inventing shapes. Reading docs is the
14
+ * 2024 answer; the 2026 one is that the agent can just ask.
15
+ *
16
+ * ## Why almost everything here is read-only
17
+ *
18
+ * An agent exploring your account should not be able to spend your money or mutate your
19
+ * avatars by accident. So the default surface is entirely reads. Everything that writes —
20
+ * starting a call, creating an avatar, syncing clips, uploading a file — is behind an
21
+ * explicit opt-in (`allowWrites`).
22
+ *
23
+ * Of those, only `start_call` spends credits, and it additionally refuses a live key. The
24
+ * rest mutate account state without billing. `upload_asset` is the other one to understand
25
+ * before arming: it reads a path off this machine's disk. Read `libs/mcp/README.md` first.
26
+ */
27
+ export interface CreateServerOptions {
28
+ apiKey: string;
29
+ baseUrl?: string;
30
+ /**
31
+ * Expose the tools that cost credits or change account state. Default `false`.
32
+ *
33
+ * Off, this server can be pointed at a production key with no more risk than a dashboard
34
+ * you left open. On, an agent can start billable calls.
35
+ */
36
+ allowWrites?: boolean;
37
+ /** Injected in tests. */
38
+ fetch?: typeof fetch;
39
+ }
40
+ export declare function createServer(options: CreateServerOptions): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,243 @@
1
+ import { openAsBlob } from "node:fs";
2
+ import { stat } from "node:fs/promises";
3
+ import { basename, extname, isAbsolute, resolve } from "node:path";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { z } from "zod";
6
+ import { RealtimeAvatar, isQueued } from "realtime-avatar";
7
+ /**
8
+ * Mirrors this package's `version`. It reaches the wire twice — as the MCP server's own
9
+ * identity, and inside the `User-Agent` — so a stale value misattributes real traffic.
10
+ * `test/server.test.ts` asserts the two stay equal; the equivalent constant in
11
+ * `realtime-avatar` had that guard and this one did not, which is how it drifted.
12
+ */
13
+ export const MCP_VERSION = "0.3.0";
14
+ const MICROS_PER_CREDIT = 1_000_000;
15
+ /** Extension -> asset kind, so a caller does not have to restate what the filename says. */
16
+ const KIND_BY_EXT = {
17
+ ".mp4": "video", ".mov": "video", ".webm": "video", ".m4v": "video",
18
+ ".png": "image", ".jpg": "image", ".jpeg": "image", ".webp": "image", ".gif": "image",
19
+ ".mp3": "audio", ".wav": "audio", ".m4a": "audio", ".ogg": "audio",
20
+ };
21
+ /** Refuse early rather than streaming a gigabyte at the API to be rejected there. */
22
+ const MAX_UPLOAD_BYTES = 512 * 1024 * 1024;
23
+ /** Credit micros as something a human reads without counting zeros. */
24
+ function credits(micros) {
25
+ if (micros === null)
26
+ return "—";
27
+ return `${(micros / MICROS_PER_CREDIT).toFixed(2)} credits`;
28
+ }
29
+ const text = (body) => ({ content: [{ type: "text", text: body }] });
30
+ /** A tool-level error the model can read and correct, not a thrown exception. */
31
+ const fail = (body) => ({
32
+ isError: true,
33
+ content: [{ type: "text", text: body }],
34
+ });
35
+ export function createServer(options) {
36
+ const rta = new RealtimeAvatar({
37
+ apiKey: options.apiKey,
38
+ baseUrl: options.baseUrl,
39
+ fetch: options.fetch,
40
+ userAgent: `realtime-avatar-mcp/${MCP_VERSION}`,
41
+ });
42
+ const isLiveKey = options.apiKey.startsWith("tic_live_");
43
+ const server = new McpServer({ name: "realtime-avatar-mcp", version: MCP_VERSION }, {
44
+ instructions: "Realtime Avatar: a live character your users can talk to. Start calls from your " +
45
+ "server, relay the connection payload to the browser untouched. Use list_avatars to " +
46
+ "find a real avatarId before writing code — never invent one. Every call is full " +
47
+ "duplex; mode:'avatar' (the default) adds video, mode:'voice' is cheaper audio-only. " +
48
+ "Billing is per second.",
49
+ });
50
+ // ── reads ──────────────────────────────────────────────────────────────────
51
+ server.registerTool("list_avatars", {
52
+ title: "List avatars",
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'.",
56
+ inputSchema: {},
57
+ annotations: { readOnlyHint: true, openWorldHint: true },
58
+ }, async () => {
59
+ const avatars = await rta.listAvatars();
60
+ if (avatars.length === 0) {
61
+ return text("No avatars yet. Create one from a video URL before starting a call.");
62
+ }
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")}`);
67
+ });
68
+ server.registerTool("get_avatar", {
69
+ title: "Get one avatar",
70
+ description: "Full detail for a single avatar, including its clip set and voice.",
71
+ inputSchema: { avatarId: z.string().describe("e.g. ava_1234…") },
72
+ annotations: { readOnlyHint: true, openWorldHint: true },
73
+ }, async ({ avatarId }) => text(JSON.stringify(await rta.getAvatar(avatarId), null, 2)));
74
+ server.registerTool("credit_balance", {
75
+ title: "Credit balance",
76
+ description: "Current balance and how much is reserved by in-flight calls.",
77
+ inputSchema: {},
78
+ annotations: { readOnlyHint: true, openWorldHint: true },
79
+ }, async () => {
80
+ const balance = await rta.creditBalance();
81
+ return text(`balance ${credits(balance.balanceCreditMicros)}\n` +
82
+ `reserved ${credits(balance.reservedCreditMicros)} (held by calls in flight)`);
83
+ });
84
+ server.registerTool("list_sessions", {
85
+ title: "List billable sessions",
86
+ description: "The itemised bill: every session with when it ran, how long it was billable for, " +
87
+ "and what it cost. Billing is per SECOND — activeSeconds is real wall time, never " +
88
+ "rounded up to a minute. Pass endUserId to see one of your own users, which works " +
89
+ "only if the call was started with metadata.user_id.",
90
+ inputSchema: {
91
+ from: z.string().optional().describe("ISO timestamp. Defaults to 30 days ago."),
92
+ to: z.string().optional().describe("ISO timestamp. Defaults to now."),
93
+ endUserId: z.string().optional().describe("Your own user id, from metadata.user_id"),
94
+ limit: z.number().int().min(1).max(200).optional(),
95
+ },
96
+ annotations: { readOnlyHint: true, openWorldHint: true },
97
+ }, async ({ from, to, endUserId, limit }) => {
98
+ const page = await rta.listSessions({ from, to, endUserId, limit });
99
+ if (page.sessions.length === 0)
100
+ return text(`No sessions between ${page.from} and ${page.to}.`);
101
+ const rows = page.sessions.map((s) => {
102
+ const seconds = s.activeSeconds === null ? "—" : `${s.activeSeconds.toFixed(1)}s`;
103
+ const user = typeof s.metadata.user_id === "string" ? s.metadata.user_id : "";
104
+ return `${(s.startedAt ?? s.createdAt).slice(0, 19)} ${seconds.padStart(8)} ${credits(s.billedCreditMicros).padStart(14)} ${user}`;
105
+ });
106
+ const total = page.sessions.reduce((sum, s) => sum + (s.billedCreditMicros ?? 0), 0);
107
+ return text(`${page.sessions.length} session(s), ${page.from.slice(0, 10)} → ${page.to.slice(0, 10)}\n\n` +
108
+ `started duration cost user\n${rows.join("\n")}\n\n` +
109
+ `total ${credits(total)}` +
110
+ (page.nextCursor ? "\n(more pages available)" : ""));
111
+ });
112
+ if (!options.allowWrites)
113
+ return server;
114
+ // ── writes, opt-in only ────────────────────────────────────────────────────
115
+ server.registerTool("start_call", {
116
+ title: "Start a call (SPENDS CREDITS)",
117
+ description: "Mint a live session to verify an integration end to end. This starts a real call " +
118
+ "and bills for it by the second. maxSeconds is the cap and you should keep it small. " +
119
+ "Returns the connection payload, which a browser client relays UNTOUCHED.",
120
+ inputSchema: {
121
+ avatarId: z.string(),
122
+ maxSeconds: z.number().int().min(1).max(300).default(60)
123
+ .describe("Hard cap. Keep it small — this is billed."),
124
+ mode: z.enum(["voice", "avatar"]).optional().describe("'avatar' for video"),
125
+ endUserId: z.string().optional().describe("Tags the session so it shows on the bill"),
126
+ },
127
+ // Not destructive (it creates nothing you must undo), but it is emphatically not
128
+ // read-only and it touches the outside world. Say so, so a host can gate it.
129
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
130
+ }, async ({ avatarId, maxSeconds, mode, endUserId }) => {
131
+ // A live key bills a real customer account. An agent poking at an integration should
132
+ // never be able to do that, whatever the operator turned on.
133
+ if (isLiveKey) {
134
+ return {
135
+ isError: true,
136
+ content: [{
137
+ type: "text",
138
+ text: "Refusing: this is a tic_live_ key and this tool spends real credits. " +
139
+ "Point the server at a tic_test_ key to start calls.",
140
+ }],
141
+ };
142
+ }
143
+ const call = await rta.startCall({
144
+ avatarId,
145
+ maxSeconds,
146
+ mode,
147
+ ...(endUserId ? { metadata: { user_id: endUserId } } : {}),
148
+ });
149
+ if (isQueued(call)) {
150
+ return text(`Queued at position ${call.position ?? "?"} — retry in ${call.retryAfterMs}ms.`);
151
+ }
152
+ return text(`Started ${call.raw.session_id}\nroom ${call.raw.room_name}\n\n` +
153
+ "Relay this payload to the browser byte-for-byte — the client SDK validates it " +
154
+ "strictly and rejects a reshaped object.");
155
+ });
156
+ server.registerTool("sync_clips", {
157
+ title: "Sync an avatar's clips",
158
+ description: "Prepare an avatar's clip set. Clips are prepared once and cached by URL hash, and " +
159
+ "the serve path only LOADS that cache — so a clip you added but never synced does " +
160
+ "nothing at all on the next call, silently. Idempotent: call it after every clip " +
161
+ "change. Pass the complete set you want live; anything omitted is retired. This does " +
162
+ "not spend credits.",
163
+ inputSchema: {
164
+ avatarId: z.string(),
165
+ clipUrls: z.array(z.string().url()).max(64)
166
+ .describe("The COMPLETE set that should be live. Omitted clips are retired."),
167
+ },
168
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
169
+ }, async ({ avatarId, clipUrls }) => {
170
+ const result = await rta.syncClips(avatarId, clipUrls);
171
+ const line = (label, urls) => urls.length ? `${label} (${urls.length})\n ${urls.join("\n ")}` : `${label} (0)`;
172
+ return text([
173
+ line("queued — preparing now", result.queued),
174
+ line("ready — already cached", result.ready),
175
+ line("retired — no longer live", result.retired),
176
+ ].join("\n") +
177
+ (result.queued.length
178
+ ? "\n\nQueued clips are not usable until they finish preparing."
179
+ : ""));
180
+ });
181
+ server.registerTool("upload_asset", {
182
+ title: "Upload a local file",
183
+ description: "Upload a file FROM THIS MACHINE'S DISK and get back a public URL usable as a clip " +
184
+ "or avatar source. Give an absolute path. Kind is inferred from the extension when " +
185
+ "you omit it. Does not spend credits. If the file is already on the internet, use " +
186
+ "create_remote_asset instead — it needs no local copy.",
187
+ inputSchema: {
188
+ path: z.string().describe("Absolute path on the machine running this server"),
189
+ kind: z.enum(["image", "video", "audio"]).optional()
190
+ .describe("Inferred from the extension when omitted"),
191
+ },
192
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
193
+ }, async ({ path, kind }) => {
194
+ // This tool reads local disk. Require an absolute path so a relative one cannot resolve
195
+ // against whatever directory the host happened to spawn the server in.
196
+ if (!isAbsolute(path)) {
197
+ return fail(`Give an absolute path — got "${path}".`);
198
+ }
199
+ const full = resolve(path);
200
+ const info = await stat(full).catch(() => null);
201
+ if (!info?.isFile())
202
+ return fail(`No file at ${full}`);
203
+ if (info.size > MAX_UPLOAD_BYTES) {
204
+ return fail(`${full} is ${(info.size / 1e6).toFixed(0)}MB — over the ${MAX_UPLOAD_BYTES / 1e6}MB limit.`);
205
+ }
206
+ const resolvedKind = kind ?? KIND_BY_EXT[extname(full).toLowerCase()];
207
+ if (!resolvedKind) {
208
+ return fail(`Cannot tell what ${extname(full) || "this file"} is — pass kind explicitly.`);
209
+ }
210
+ const asset = await rta.uploadAsset(await openAsBlob(full), {
211
+ kind: resolvedKind,
212
+ filename: basename(full),
213
+ });
214
+ return text(`Uploaded ${asset.id} (${resolvedKind}, ${(info.size / 1e6).toFixed(1)}MB)\n${asset.url}`);
215
+ });
216
+ server.registerTool("create_remote_asset", {
217
+ title: "Register a file already on the internet",
218
+ description: "Point at a public URL and get an asset back without downloading it locally. Use " +
219
+ "this instead of upload_asset whenever the file is already hosted somewhere.",
220
+ inputSchema: {
221
+ remoteUrl: z.string().url(),
222
+ kind: z.enum(["image", "video", "audio"]),
223
+ },
224
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
225
+ }, async ({ remoteUrl, kind }) => {
226
+ const asset = await rta.createRemoteAsset({ kind, remoteUrl });
227
+ return text(`Created ${asset.id} (${kind})\n${asset.url}`);
228
+ });
229
+ server.registerTool("create_avatar_from_video", {
230
+ title: "Create an avatar from a video",
231
+ description: "Build an avatar from a looping video URL. Use a VIDEO source: an avatar built from " +
232
+ "a still image reaches 'ready' and then publishes a black track on every call.",
233
+ inputSchema: {
234
+ displayName: z.string(),
235
+ videoUrl: z.string().url().describe("Publicly reachable mp4, opening and closing on the same rest pose"),
236
+ },
237
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
238
+ }, async ({ displayName, videoUrl }) => {
239
+ const avatar = await rta.createAvatarFromVideo({ displayName, videoUrl });
240
+ return text(`Created ${avatar.id} (${avatar.status}). Poll get_avatar until it is ready.`);
241
+ });
242
+ return server;
243
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "realtime-avatar-mcp",
3
+ "version": "0.3.0",
4
+ "description": "MCP server for Realtime Avatar — let a coding agent read your avatars, balance and bill directly.",
5
+ "license": "MIT",
6
+ "author": "The Influence Company",
7
+ "homepage": "https://realtimeavatar.ai/docs",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/theinfluencecompany/realtime-avatar-sdk.git",
11
+ "directory": "libs/mcp"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/theinfluencecompany/realtime-avatar-sdk/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "avatar",
19
+ "realtime",
20
+ "voice",
21
+ "video",
22
+ "agent",
23
+ "full-duplex",
24
+ "livekit",
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "claude",
28
+ "coding-agent"
29
+ ],
30
+ "type": "module",
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ }
38
+ },
39
+ "bin": {
40
+ "realtime-avatar-mcp": "./dist/bin.js"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "README.md"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "sideEffects": false,
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.build.json"
55
+ },
56
+ "dependencies": {
57
+ "@modelcontextprotocol/sdk": "^1.30.0",
58
+ "realtime-avatar": "0.3.0",
59
+ "zod": "^4.4.3"
60
+ }
61
+ }