@three-ws/avatar-agent 1.0.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.
@@ -0,0 +1,219 @@
1
+ // `inspect_glb` — fetch any GLB URL and return its full structural
2
+ // breakdown: meshes, materials, textures, animations, file size, vertex
3
+ // + triangle counts, bounding box, and skinning info.
4
+ //
5
+ // Pure parse via @gltf-transform/core. No network calls beyond the GLB
6
+ // fetch itself.
7
+
8
+ import { z } from 'zod';
9
+
10
+ import { fetchGlbBytes, getIo } from '../lib/glb-io.js';
11
+
12
+ function arrayLen(prim, semantic) {
13
+ const acc = prim.getAttribute(semantic);
14
+ return acc ? acc.getCount() : 0;
15
+ }
16
+
17
+ function primitiveTriangleCount(prim) {
18
+ const mode = prim.getMode();
19
+ const idx = prim.getIndices();
20
+ const count = idx ? idx.getCount() : arrayLen(prim, 'POSITION');
21
+ // glTF primitive modes: 0=POINTS, 1=LINES, 4=TRIANGLES, 5=TRIANGLE_STRIP, 6=TRIANGLE_FAN.
22
+ switch (mode) {
23
+ case 4:
24
+ return Math.floor(count / 3);
25
+ case 5:
26
+ case 6:
27
+ return Math.max(0, count - 2);
28
+ default:
29
+ return 0;
30
+ }
31
+ }
32
+
33
+ function computeWorldBbox(scene) {
34
+ let min = [Infinity, Infinity, Infinity];
35
+ let max = [-Infinity, -Infinity, -Infinity];
36
+ const visit = (node, mat = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]) => {
37
+ // Compose: world = parent * local. Keep mat as a 16-element column-major
38
+ // transform; we only need translate + axis-aligned vertex check so
39
+ // the full matrix multiplication suffices.
40
+ const t = node.getMatrix();
41
+ const world = mul(mat, t);
42
+ const mesh = node.getMesh();
43
+ if (mesh) {
44
+ for (const prim of mesh.listPrimitives()) {
45
+ const pos = prim.getAttribute('POSITION');
46
+ if (!pos) continue;
47
+ const arr = pos.getArray();
48
+ const stride = pos.getElementSize();
49
+ for (let i = 0; i < arr.length; i += stride) {
50
+ const v = transform([arr[i], arr[i + 1], arr[i + 2]], world);
51
+ if (v[0] < min[0]) min[0] = v[0];
52
+ if (v[1] < min[1]) min[1] = v[1];
53
+ if (v[2] < min[2]) min[2] = v[2];
54
+ if (v[0] > max[0]) max[0] = v[0];
55
+ if (v[1] > max[1]) max[1] = v[1];
56
+ if (v[2] > max[2]) max[2] = v[2];
57
+ }
58
+ }
59
+ }
60
+ for (const child of node.listChildren()) visit(child, world);
61
+ };
62
+ for (const root of scene.listChildren()) visit(root);
63
+ if (!Number.isFinite(min[0])) return null;
64
+ return {
65
+ min,
66
+ max,
67
+ center: [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2],
68
+ size: [max[0] - min[0], max[1] - min[1], max[2] - min[2]],
69
+ };
70
+ }
71
+
72
+ function mul(a, b) {
73
+ const r = new Array(16).fill(0);
74
+ for (let i = 0; i < 4; i++) {
75
+ for (let j = 0; j < 4; j++) {
76
+ let s = 0;
77
+ for (let k = 0; k < 4; k++) s += a[i + k * 4] * b[k + j * 4];
78
+ r[i + j * 4] = s;
79
+ }
80
+ }
81
+ return r;
82
+ }
83
+
84
+ function transform(v, m) {
85
+ return [
86
+ m[0] * v[0] + m[4] * v[1] + m[8] * v[2] + m[12],
87
+ m[1] * v[0] + m[5] * v[1] + m[9] * v[2] + m[13],
88
+ m[2] * v[0] + m[6] * v[1] + m[10] * v[2] + m[14],
89
+ ];
90
+ }
91
+
92
+ export const def = {
93
+ name: 'inspect_glb',
94
+ title: 'Inspect a GLB / glTF 3D model',
95
+ description:
96
+ 'Fetch any GLB URL (or data: URL) and return a full structural breakdown: meshes, primitives, materials, textures, animations, skins, vertex + triangle counts, world-space bounding box, and file size. Pure local parse via @gltf-transform/core — no third-party services.',
97
+ inputSchema: {
98
+ url: z.string().describe('Public URL or data: URL of a .glb file.'),
99
+ },
100
+ async handler(args) {
101
+ const { url } = args || {};
102
+ if (!url) return { ok: false, error: 'invalid_input', message: 'url is required' };
103
+ let bytes;
104
+ let doc;
105
+ try {
106
+ bytes = await fetchGlbBytes(url);
107
+ const io = await getIo();
108
+ doc = await io.readBinary(bytes);
109
+ } catch (err) {
110
+ return { ok: false, error: 'parse_failed', message: err.message };
111
+ }
112
+ const root = doc.getRoot();
113
+ const asset = root.getAsset();
114
+
115
+ let totalVertices = 0;
116
+ let totalTriangles = 0;
117
+ const meshes = root.listMeshes().map((mesh) => {
118
+ const prims = mesh.listPrimitives().map((p) => {
119
+ const v = arrayLen(p, 'POSITION');
120
+ const tri = primitiveTriangleCount(p);
121
+ totalVertices += v;
122
+ totalTriangles += tri;
123
+ return {
124
+ mode: p.getMode(),
125
+ vertices: v,
126
+ triangles: tri,
127
+ indexed: !!p.getIndices(),
128
+ attributes: p.listSemantics(),
129
+ material: p.getMaterial()?.getName() || null,
130
+ morphTargets: p.listTargets().length,
131
+ };
132
+ });
133
+ return {
134
+ name: mesh.getName() || null,
135
+ primitiveCount: prims.length,
136
+ primitives: prims,
137
+ };
138
+ });
139
+
140
+ const materials = root.listMaterials().map((m) => ({
141
+ name: m.getName() || null,
142
+ alphaMode: m.getAlphaMode(),
143
+ doubleSided: m.getDoubleSided(),
144
+ baseColorFactor: m.getBaseColorFactor(),
145
+ metallicFactor: m.getMetallicFactor(),
146
+ roughnessFactor: m.getRoughnessFactor(),
147
+ hasBaseColorTexture: !!m.getBaseColorTexture(),
148
+ hasMetallicRoughnessTexture: !!m.getMetallicRoughnessTexture(),
149
+ hasNormalTexture: !!m.getNormalTexture(),
150
+ hasOcclusionTexture: !!m.getOcclusionTexture(),
151
+ hasEmissiveTexture: !!m.getEmissiveTexture(),
152
+ }));
153
+
154
+ const textures = root.listTextures().map((t) => ({
155
+ name: t.getName() || null,
156
+ mimeType: t.getMimeType(),
157
+ sizeBytes: t.getImage()?.byteLength ?? null,
158
+ uri: t.getURI() || null,
159
+ }));
160
+
161
+ const animations = root.listAnimations().map((a) => {
162
+ let maxTime = 0;
163
+ for (const ch of a.listChannels()) {
164
+ const sampler = ch.getSampler();
165
+ const input = sampler?.getInput();
166
+ if (input) {
167
+ const arr = input.getArray();
168
+ const last = arr[arr.length - 1];
169
+ if (last > maxTime) maxTime = last;
170
+ }
171
+ }
172
+ return {
173
+ name: a.getName() || null,
174
+ channelCount: a.listChannels().length,
175
+ samplerCount: a.listSamplers().length,
176
+ durationSeconds: maxTime,
177
+ };
178
+ });
179
+
180
+ const skins = root.listSkins().map((s) => ({
181
+ name: s.getName() || null,
182
+ jointCount: s.listJoints().length,
183
+ hasInverseBindMatrices: !!s.getInverseBindMatrices(),
184
+ }));
185
+
186
+ const scenes = root.listScenes().map((s) => ({
187
+ name: s.getName() || null,
188
+ rootNodeCount: s.listChildren().length,
189
+ }));
190
+
191
+ const defaultScene = root.getDefaultScene() || root.listScenes()[0] || null;
192
+ const bbox = defaultScene ? computeWorldBbox(defaultScene) : null;
193
+
194
+ return {
195
+ ok: true,
196
+ url,
197
+ sizeBytes: bytes.byteLength,
198
+ generator: asset.generator || null,
199
+ version: asset.version || null,
200
+ counts: {
201
+ meshes: meshes.length,
202
+ materials: materials.length,
203
+ textures: textures.length,
204
+ animations: animations.length,
205
+ skins: skins.length,
206
+ scenes: scenes.length,
207
+ totalVertices,
208
+ totalTriangles,
209
+ },
210
+ boundingBox: bbox,
211
+ meshes,
212
+ materials,
213
+ textures,
214
+ animations,
215
+ skins,
216
+ scenes,
217
+ };
218
+ },
219
+ };
@@ -0,0 +1,37 @@
1
+ // `list_animations` — fetch the three.ws pose preset catalog (24 presets
2
+ // across Standing, Action, Sitting & Floor, Expressive). Lives at
3
+ // GET /api/render/avatar-clip on three.ws so the catalog stays
4
+ // authoritative even when this MCP version is older than the live one.
5
+
6
+ import { THREE_WS_BASE } from '../config.js';
7
+ import { fetchPoseCatalogRemote } from '../lib/render.js';
8
+
9
+ export const def = {
10
+ name: 'list_animations',
11
+ title: 'List three.ws pose presets + animation slots',
12
+ description:
13
+ 'Return the three.ws pose preset catalog (T-pose, A-pose, wave, thinker, jump, dance, warrior2, …) grouped by category. Use the ids returned here as posePresetId in render_avatar. Fetched live from three.ws so the catalog reflects the deployed version.',
14
+ inputSchema: {},
15
+ async handler() {
16
+ try {
17
+ const catalog = await fetchPoseCatalogRemote();
18
+ const grouped = {};
19
+ for (const p of catalog.poses || []) {
20
+ const key = p.group || 'Other';
21
+ if (!grouped[key]) grouped[key] = [];
22
+ grouped[key].push({ id: p.id, label: p.label });
23
+ }
24
+ return {
25
+ ok: true,
26
+ source: `${THREE_WS_BASE}/api/render/avatar-clip`,
27
+ poseCount: (catalog.poses || []).length,
28
+ groups: Object.keys(grouped).map((g) => ({ group: g, poses: grouped[g] })),
29
+ cameraOrbit: catalog.cameraOrbit,
30
+ background: catalog.background,
31
+ fetchedAt: new Date().toISOString(),
32
+ };
33
+ } catch (err) {
34
+ return { ok: false, error: 'fetch_failed', message: err.message };
35
+ }
36
+ },
37
+ };
@@ -0,0 +1,20 @@
1
+ // `list_avatars` — enumerate the curated default avatars + accessories +
2
+ // pose presets shipped by three.ws. Free, no signer needed.
3
+
4
+ import { ACCESSORIES, DEFAULT_AVATARS, POSE_PRESETS } from '../lib/avatars.js';
5
+
6
+ export const def = {
7
+ name: 'list_avatars',
8
+ title: 'List three.ws default avatars + accessories',
9
+ description:
10
+ 'Return the catalog of default 3D avatars (default, cz) and accessories (hats, glasses, earrings) hosted on the three.ws CDN. Each entry includes a public GLB URL ready to load in any glTF viewer or Three.js scene. Includes the supported pose preset names.',
11
+ inputSchema: {},
12
+ async handler() {
13
+ return {
14
+ avatars: DEFAULT_AVATARS,
15
+ accessories: ACCESSORIES,
16
+ poses: POSE_PRESETS,
17
+ fetchedAt: new Date().toISOString(),
18
+ };
19
+ },
20
+ };
@@ -0,0 +1,114 @@
1
+ // `optimize_glb` — shrink a GLB by running a configurable
2
+ // @gltf-transform/functions pipeline: dedup → prune → join → weld
3
+ // → Draco mesh compression. Returns the optimized bytes as a base64
4
+ // data URL plus before/after byte counts so callers see the saving.
5
+ //
6
+ // Texture re-encoding (PNG → WebP/AVIF) is intentionally off by default
7
+ // because it requires Sharp's native binaries; flip `reencodeTextures`
8
+ // when running in an env where Sharp is available.
9
+
10
+ import { z } from 'zod';
11
+ import { dedup, draco, prune, weld } from '@gltf-transform/functions';
12
+
13
+ import { fetchGlbBytes, getIo } from '../lib/glb-io.js';
14
+
15
+ const MAX_RETURN_BYTES = 20 * 1024 * 1024;
16
+
17
+ export const def = {
18
+ name: 'optimize_glb',
19
+ title: 'Optimize a GLB (dedup, prune, weld, Draco)',
20
+ description:
21
+ 'Run a @gltf-transform/functions optimization pipeline on a GLB URL: dedup → prune unused → weld duplicate vertices → optional Draco mesh compression. Returns the optimized bytes as a base64 data URL with before/after sizes. Lossless for geometry except where Draco quantization is requested.',
22
+ inputSchema: {
23
+ url: z.string().describe('Source GLB URL (or data: URL).'),
24
+ dedup: z.boolean().optional().describe('Merge equivalent accessors / materials / textures. Default true.'),
25
+ prune: z.boolean().optional().describe('Remove unused materials / nodes / meshes. Default true.'),
26
+ weld: z.boolean().optional().describe('Merge duplicate vertices. Default true.'),
27
+ draco: z.boolean().optional().describe('Apply Draco mesh compression (lossy quantization). Default false.'),
28
+ dracoQuantizePosition: z.number().int().min(1).max(16).optional()
29
+ .describe('Position bits for Draco (default 14 = high fidelity).'),
30
+ returnInline: z.boolean().optional()
31
+ .describe('Return the optimized GLB inline as a base64 data URL (default true). Set false to return only stats.'),
32
+ },
33
+ async handler(args) {
34
+ const { url } = args || {};
35
+ if (!url) return { ok: false, error: 'invalid_input', message: 'url is required' };
36
+ const wantDedup = args.dedup !== false;
37
+ const wantPrune = args.prune !== false;
38
+ const wantWeld = args.weld !== false;
39
+ const wantDraco = !!args.draco;
40
+ const inline = args.returnInline !== false;
41
+
42
+ let srcBytes;
43
+ try {
44
+ srcBytes = await fetchGlbBytes(url);
45
+ } catch (err) {
46
+ return { ok: false, error: 'fetch_failed', message: err.message };
47
+ }
48
+
49
+ let doc;
50
+ const io = await getIo();
51
+ try {
52
+ doc = await io.readBinary(srcBytes);
53
+ } catch (err) {
54
+ return { ok: false, error: 'parse_failed', message: err.message };
55
+ }
56
+
57
+ const applied = [];
58
+ try {
59
+ if (wantDedup) {
60
+ await doc.transform(dedup());
61
+ applied.push('dedup');
62
+ }
63
+ if (wantPrune) {
64
+ await doc.transform(prune());
65
+ applied.push('prune');
66
+ }
67
+ if (wantWeld) {
68
+ await doc.transform(weld());
69
+ applied.push('weld');
70
+ }
71
+ if (wantDraco) {
72
+ const opts = {};
73
+ if (typeof args.dracoQuantizePosition === 'number') {
74
+ opts.quantizePosition = args.dracoQuantizePosition;
75
+ }
76
+ await doc.transform(draco(opts));
77
+ applied.push('draco');
78
+ }
79
+ } catch (err) {
80
+ return { ok: false, error: 'transform_failed', message: err.message, applied };
81
+ }
82
+
83
+ let outBytes;
84
+ try {
85
+ outBytes = await io.writeBinary(doc);
86
+ } catch (err) {
87
+ return { ok: false, error: 'write_failed', message: err.message, applied };
88
+ }
89
+
90
+ const before = srcBytes.byteLength;
91
+ const after = outBytes.byteLength;
92
+ const ratio = before > 0 ? after / before : 1;
93
+ const result = {
94
+ ok: true,
95
+ url,
96
+ pipeline: applied,
97
+ beforeBytes: before,
98
+ afterBytes: after,
99
+ savedBytes: before - after,
100
+ ratio,
101
+ reductionPct: (1 - ratio) * 100,
102
+ };
103
+ if (inline) {
104
+ if (after > MAX_RETURN_BYTES) {
105
+ result.optimizedGlb = null;
106
+ result.note = `Optimized GLB is ${after} bytes (> ${MAX_RETURN_BYTES} inline cap). Re-run with smaller input or use a hosting endpoint.`;
107
+ } else {
108
+ const b64 = Buffer.from(outBytes).toString('base64');
109
+ result.optimizedGlb = `data:model/gltf-binary;base64,${b64}`;
110
+ }
111
+ }
112
+ return result;
113
+ },
114
+ };
@@ -0,0 +1,69 @@
1
+ // `pump_buy` — buy any Solana SPL or pump.fun token via Jupiter.
2
+ //
3
+ // Two modes:
4
+ // - direct (default): the buyer wallet pays its own fee + spends SOL.
5
+ // - bundled (set jitoBundle=true with a funderSecret): two-tx Jito
6
+ // bundle where the funder transfers SOL to the buyer + Jito tip in
7
+ // Tx1 and the buyer signs the swap in Tx2. Use this if the buyer
8
+ // wallet is shared/leaked and you must beat sweeper bots.
9
+ //
10
+ // EXECUTION ACTION — real swaps on Solana mainnet.
11
+
12
+ import { z } from 'zod';
13
+
14
+ import { isValidPubkey } from '../lib/solana.js';
15
+ import { jupiterBuyBundled, jupiterBuyDirect } from '../lib/jupiter-buy.js';
16
+ import { THREE_MINT } from '../config.js';
17
+
18
+ export const def = {
19
+ name: 'pump_buy',
20
+ title: 'Buy a Solana token via Jupiter (direct or Jito-bundled)',
21
+ description:
22
+ 'Swap SOL → target mint via Jupiter aggregator. Direct mode = one tx signed by the buyer. Bundled mode (jitoBundle=true) = two-tx Jito bundle where funderSecret transfers SOL + tip to the buyer atomically with the swap (sweeper-resistant). Pass target="three" to use the $three reference mint. EXECUTION ACTION.',
23
+ inputSchema: {
24
+ target: z.string().describe('Target mint (base58) or "three" to use the THREE_MINT env.'),
25
+ buySol: z.number().positive().describe('Amount of SOL to spend.'),
26
+ buyerSecret: z.string().describe('Base58 secret of the buyer wallet (signs the swap).'),
27
+ funderSecret: z.string().optional().describe('Base58 secret of the funder. Required when jitoBundle=true.'),
28
+ jitoBundle: z.boolean().optional().describe('Use a Jito bundle (atomic funder→buyer transfer + swap). Default false.'),
29
+ slippageBps: z.number().int().min(1).max(10_000).optional().describe('Slippage in basis points (default 500 = 5%).'),
30
+ jitoTipSol: z.number().min(0).optional().describe('Jito tip in SOL (default 0.005). Only used when jitoBundle=true.'),
31
+ priorityMicroLamports: z.number().int().min(0).max(20_000_000).optional()
32
+ .describe('Compute-unit price (default 2_000_000).'),
33
+ },
34
+ async handler(args) {
35
+ let target = args.target;
36
+ if (target === 'three' || target === '$three') {
37
+ if (!THREE_MINT) return { ok: false, error: 'three_mint_not_configured' };
38
+ target = THREE_MINT;
39
+ }
40
+ if (!isValidPubkey(target)) return { ok: false, error: 'invalid_target' };
41
+ try {
42
+ if (args.jitoBundle) {
43
+ if (!args.funderSecret) {
44
+ return { ok: false, error: 'invalid_input', message: 'jitoBundle=true requires funderSecret.' };
45
+ }
46
+ const out = await jupiterBuyBundled({
47
+ funderSecret: args.funderSecret,
48
+ buyerSecret: args.buyerSecret,
49
+ targetMint: target,
50
+ buySol: args.buySol,
51
+ slippageBps: args.slippageBps,
52
+ jitoTipSol: args.jitoTipSol,
53
+ priorityMicroLamports: args.priorityMicroLamports,
54
+ });
55
+ return out;
56
+ }
57
+ const out = await jupiterBuyDirect({
58
+ buyerSecret: args.buyerSecret,
59
+ targetMint: target,
60
+ buySol: args.buySol,
61
+ slippageBps: args.slippageBps,
62
+ priorityMicroLamports: args.priorityMicroLamports,
63
+ });
64
+ return out;
65
+ } catch (err) {
66
+ return { ok: false, error: err.code || 'buy_failed', message: err.message };
67
+ }
68
+ },
69
+ };
@@ -0,0 +1,48 @@
1
+ // `pump_collect_fees` — atomically collect a pump.fun coin's creator-fee
2
+ // vault and route the SOL to a safe destination, all in a single tx
3
+ // inside a Jito bundle.
4
+ //
5
+ // Useful when the creator key is shared or potentially leaked: even if
6
+ // another holder of the key tries to collect concurrently, the bundle's
7
+ // atomicity prevents any tx from interleaving between the collect and
8
+ // the drain.
9
+ //
10
+ // EXECUTION ACTION.
11
+
12
+ import { z } from 'zod';
13
+
14
+ import { atomicCollect } from '../lib/atomic-collect.js';
15
+
16
+ export const def = {
17
+ name: 'pump_collect_fees',
18
+ title: 'Atomic pump.fun creator-fee collection (Jito bundle)',
19
+ description:
20
+ 'Collect a pump.fun coin\'s creator-fee vault and route the SOL to a safe destination, atomically, in a single tx inside a Jito bundle. Funder pays the fee + Jito tip; creator signs collectCoinCreatorFee + drain to DESTINATION. The bundle\'s atomicity blocks any competing collector from interleaving even if the creator key is leaked. EXECUTION ACTION.',
21
+ inputSchema: {
22
+ funderSecret: z.string().describe('Base58 secret of the funder (pays fee + tip).'),
23
+ creatorSecret: z.string().describe('Base58 secret of the coin creator (signs collect + drain).'),
24
+ destination: z.string().describe('Pubkey to receive the collected SOL.'),
25
+ jitoTipSol: z.number().min(0).optional().describe('Jito tip in SOL (default 0.005).'),
26
+ priorityMicroLamports: z.number().int().min(0).max(20_000_000).optional()
27
+ .describe('Compute-unit priority price (default 3_000_000).'),
28
+ bufferLamports: z.number().int().min(0).optional()
29
+ .describe('Lamports to leave in the creator wallet (default 890880, rent-exempt minimum).'),
30
+ minVaultSol: z.number().min(0).optional()
31
+ .describe('Abort if the vault holds less than this (default 0.001).'),
32
+ },
33
+ async handler(args) {
34
+ try {
35
+ return await atomicCollect({
36
+ funderSecret: args.funderSecret,
37
+ creatorSecret: args.creatorSecret,
38
+ destination: args.destination,
39
+ jitoTipSol: args.jitoTipSol,
40
+ priorityMicroLamports: args.priorityMicroLamports,
41
+ bufferLamports: args.bufferLamports,
42
+ minVaultSol: args.minVaultSol,
43
+ });
44
+ } catch (err) {
45
+ return { ok: false, error: err.code || 'collect_failed', message: err.message };
46
+ }
47
+ },
48
+ };
@@ -0,0 +1,77 @@
1
+ // `pump_launch` — atomic pump.fun token launch via a Jito bundle.
2
+ //
3
+ // Wraps the atomic-launch port (originally from nirholas/atomic). Two-tx
4
+ // bundle: funder pays its own fee + the Jito tip and rent-funds the
5
+ // creator; creator signs createV2 in tx2. Either both land or neither
6
+ // does, so the on-chain `creator` is the creator wallet (not the funder)
7
+ // without forcing the creator to hold SOL up front.
8
+ //
9
+ // If `uri` is omitted, we upload metadata to pump.fun's IPFS endpoint
10
+ // first using the supplied name/symbol/description/socials/imageUrl. This
11
+ // makes the tool a one-shot "launch from scratch".
12
+ //
13
+ // EXECUTION ACTION — creates a real mint on Solana mainnet and pays
14
+ // Jito tips + rent.
15
+
16
+ import { z } from 'zod';
17
+
18
+ import { atomicLaunch, uploadPumpMetadata } from '../lib/atomic-launch.js';
19
+
20
+ export const def = {
21
+ name: 'pump_launch',
22
+ title: 'Atomic pump.fun launch (Jito bundle, separate funder/creator)',
23
+ description:
24
+ 'Launch a pump.fun token atomically via a Jito bundle. Funder pays its own fee + tip and rent-funds the creator; creator signs createV2 in tx2 — both txs land in the same block or neither does. If uri is omitted, metadata is uploaded to pump.fun IPFS first from name/symbol/description/socials/imageUrl. Returns the mint address, bundle id, both tx signatures, and the pump.fun URL. EXECUTION ACTION — creates a real mint on mainnet.',
25
+ inputSchema: {
26
+ name: z.string().min(1).max(32).describe('Token name.'),
27
+ symbol: z.string().min(1).max(10).describe('Token symbol (ticker).'),
28
+ funderSecret: z.string().describe('Base58 secret of the funder wallet (pays Tx1 fee + tip + rent transfer).'),
29
+ creatorSecret: z.string().describe('Base58 secret of the creator wallet (signs createV2 — becomes on-chain creator).'),
30
+ uri: z.string().url().optional().describe('Existing metadata URI. If omitted, metadata is uploaded first.'),
31
+ description: z.string().max(500).optional(),
32
+ twitter: z.string().optional(),
33
+ telegram: z.string().optional(),
34
+ website: z.string().optional(),
35
+ imageUrl: z.string().url().optional().describe('Image to upload as the token icon (re-fetched at upload time).'),
36
+ mintSecret: z.string().optional().describe('Base58 secret to use as the mint keypair (default: random).'),
37
+ rentSol: z.number().min(0).optional().describe('SOL the funder transfers to the creator for tx2 rent + fees (default 0.035).'),
38
+ jitoTipSol: z.number().min(0).optional().describe('Jito tip in SOL (default 0.005).'),
39
+ priorityMicroLamports: z.number().int().min(0).max(20_000_000).optional()
40
+ .describe('Compute-unit priority price (default 2_000_000).'),
41
+ },
42
+ async handler(args) {
43
+ try {
44
+ let uri = args.uri;
45
+ let uploadedMeta = null;
46
+ if (!uri) {
47
+ uploadedMeta = await uploadPumpMetadata({
48
+ name: args.name,
49
+ symbol: args.symbol,
50
+ description: args.description || '',
51
+ twitter: args.twitter || '',
52
+ telegram: args.telegram || '',
53
+ website: args.website || '',
54
+ imageUrl: args.imageUrl,
55
+ });
56
+ uri = uploadedMeta.uri;
57
+ if (!uri) {
58
+ return { ok: false, error: 'metadata_upload_failed', detail: uploadedMeta.raw };
59
+ }
60
+ }
61
+ const out = await atomicLaunch({
62
+ name: args.name,
63
+ symbol: args.symbol,
64
+ uri,
65
+ funderSecret: args.funderSecret,
66
+ creatorSecret: args.creatorSecret,
67
+ mintSecret: args.mintSecret,
68
+ rentSol: args.rentSol,
69
+ jitoTipSol: args.jitoTipSol,
70
+ priorityMicroLamports: args.priorityMicroLamports,
71
+ });
72
+ return { ...out, metadataUri: uri, metadataUploadedNow: !!uploadedMeta };
73
+ } catch (err) {
74
+ return { ok: false, error: err.code || 'launch_failed', message: err.message };
75
+ }
76
+ },
77
+ };
@@ -0,0 +1,38 @@
1
+ // `pump_snapshot` — live market snapshot for a Solana SPL or pump.fun
2
+ // token. Free, read-only. Aggregates Jupiter price, Dexscreener volume,
3
+ // pump.fun metadata, top holders from Solana RPC, and (optionally)
4
+ // Helius DAS supply info when HELIUS_API_KEY is set.
5
+
6
+ import { z } from 'zod';
7
+
8
+ import { snapshot } from '../lib/pumpfun.js';
9
+ import { isValidPubkey } from '../lib/solana.js';
10
+ import { THREE_MINT } from '../config.js';
11
+
12
+ export const def = {
13
+ name: 'pump_snapshot',
14
+ title: 'Live pump.fun / Solana token snapshot',
15
+ description:
16
+ 'Live snapshot for a Solana token (SPL or pump.fun): USD price (Jupiter), 24h volume + primary DEX (Dexscreener), pump.fun metadata (name/symbol/image/socials/mcap), and top-holder distribution from Solana RPC. Optional Helius DAS supply when HELIUS_API_KEY is configured. Free — no signer.',
17
+ inputSchema: {
18
+ token: z.string().min(32).max(64).describe('Base58 Solana mint address. Pass "three" or omit to use the $three reference mint (THREE_MINT env).').optional(),
19
+ },
20
+ async handler(args) {
21
+ let mint = args?.token;
22
+ if (!mint || mint === 'three' || mint === '$three') {
23
+ if (!THREE_MINT) {
24
+ return {
25
+ ok: false,
26
+ error: 'three_mint_not_configured',
27
+ message: 'No token argument and THREE_MINT env is unset. Pass token=<mint> or set THREE_MINT.',
28
+ };
29
+ }
30
+ mint = THREE_MINT;
31
+ }
32
+ if (!isValidPubkey(mint)) {
33
+ return { ok: false, error: 'invalid_mint', token: mint };
34
+ }
35
+ const result = await snapshot(mint);
36
+ return { ok: true, ...result };
37
+ },
38
+ };
@@ -0,0 +1,60 @@
1
+ // `render_avatar` — full posed avatar render with three.ws's render-clip
2
+ // pipeline: applies a pose preset's Euler-rotation map to the rig, sets
3
+ // the camera orbit (theta/phi/radius), optionally applies ARKit-52 morph
4
+ // targets for facial expression, and returns the PNG.
5
+ //
6
+ // Mirrors the experience of the three.ws customizer: same rig conventions
7
+ // (Avaturn), same pose library (PRESETS in /src/pose-presets.js), same
8
+ // lighting (three-light rig with ACES tone mapping). Works on any GLB
9
+ // that follows the standard rig — both curated defaults and uploaded
10
+ // avatars.
11
+
12
+ import { z } from 'zod';
13
+
14
+ import { renderAvatarClipRemote } from '../lib/render.js';
15
+ import { getSession } from '../lib/avatars.js';
16
+
17
+ export const def = {
18
+ name: 'render_avatar',
19
+ title: 'Render a posed avatar (pose + camera + expression)',
20
+ description:
21
+ 'Render an avatar GLB with a three.ws pose preset, camera orbit, and optional ARKit-52 facial expression. Same pipeline as the three.ws customizer\'s save-snapshot flow. Returns PNG bytes as a base64 data URL. Accepts either glbUrl directly or sessionId from spawn_avatar.',
22
+ inputSchema: {
23
+ glbUrl: z.string().url().optional().describe('Avatar GLB URL. Required if sessionId is omitted.'),
24
+ sessionId: z.string().optional().describe('Avatar session id from spawn_avatar. Overrides glbUrl.'),
25
+ posePresetId: z.string().optional()
26
+ .describe('Pose preset id (e.g. "tpose", "wave", "thinker"). Call list_animations for the full catalog.'),
27
+ cameraOrbit: z.object({
28
+ theta: z.number().optional().describe('Yaw in degrees, 0..360. Default 0 (front).'),
29
+ phi: z.number().optional().describe('Pitch in degrees, 0..180 (from top). Default 80 (slightly above eye-level).'),
30
+ radius: z.number().nullable().optional().describe('Distance in meters. null = auto-frame the bounding box.'),
31
+ }).optional().describe('Camera orbit around the avatar.'),
32
+ expression: z.record(z.number()).optional()
33
+ .describe('ARKit-52 morph target map, e.g. { mouthSmileLeft: 0.6, mouthSmileRight: 0.6 }.'),
34
+ width: z.number().int().min(64).max(2048).optional().describe('Output width (default 1024).'),
35
+ height: z.number().int().min(64).max(2048).optional().describe('Output height (default 1024).'),
36
+ background: z.string().optional().describe('CSS color or "transparent". Default "#0a0a0a".'),
37
+ },
38
+ async handler(args) {
39
+ const { sessionId, glbUrl, posePresetId, cameraOrbit, expression, width, height, background } = args || {};
40
+ let resolvedGlb = glbUrl;
41
+ let sessionPose = null;
42
+ if (sessionId) {
43
+ const session = getSession(sessionId);
44
+ if (!session) return { ok: false, error: 'unknown_session', message: `No session ${sessionId}.` };
45
+ resolvedGlb = session.avatar.glb;
46
+ sessionPose = session.pose && session.pose !== 'idle' ? session.pose : null;
47
+ }
48
+ if (!resolvedGlb) return { ok: false, error: 'invalid_input', message: 'Pass glbUrl or sessionId.' };
49
+ const out = await renderAvatarClipRemote({
50
+ glbUrl: resolvedGlb,
51
+ posePresetId: posePresetId || sessionPose || null,
52
+ cameraOrbit,
53
+ expression,
54
+ width,
55
+ height,
56
+ background,
57
+ });
58
+ return { ...out, glbUrl: resolvedGlb, sessionId: sessionId || null };
59
+ },
60
+ };