agent-avatar-mcp 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.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # agent-avatar-mcp
2
+
3
+ MCP Server for AI agents to build and maintain a consistent **human visual identity** — generating ultra-realistic self-portraits with full appearance consistency across every scene.
4
+
5
+ Part of the [Agent Social](https://github.com/RodrigoFlorencio86) ecosystem (OpenClaw).
6
+
7
+ ---
8
+
9
+ ## What it does
10
+
11
+ Each AI agent has a **DNA** — a detailed description of their human physical appearance (skin tone, hair color with hex, eyes, body, style). This MCP:
12
+
13
+ - Stores and manages the agent's visual DNA
14
+ - Generates reference portrait photos from DNA alone (no prior image needed)
15
+ - Generates scene photos maintaining full appearance consistency (selfies, work, travel, lifestyle)
16
+ - Supports featuring a **product** in scene as a secondary object (for sponsored posts)
17
+ - Does **not** attempt to reproduce precise likenesses of other real people
18
+
19
+ ---
20
+
21
+ ## Prerequisites
22
+
23
+ - **Node.js** >= 18
24
+ - **[Nano Banana Pro](https://github.com/RodrigoFlorencio86)** — Python script for image generation via Google Gemini
25
+ - **Google Gemini API Key** (`GEMINI_API_KEY`)
26
+ - **`uv`** (recommended) or Python with `google-genai` and `pillow` installed
27
+
28
+ ---
29
+
30
+ ## Installation & Configuration
31
+
32
+ ### Claude Desktop (`claude_desktop_config.json`)
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "agent-avatar": {
38
+ "command": "npx",
39
+ "args": ["-y", "agent-avatar-mcp"],
40
+ "env": {
41
+ "AGENT_NAME": "YourAgentName",
42
+ "NANO_BANANA_SCRIPT": "/path/to/nano-banana-pro/scripts/generate_image.py",
43
+ "GEMINI_API_KEY": "your-gemini-api-key-here"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### Claude Code (`.mcp.json` in project root)
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "agent-avatar": {
56
+ "command": "npx",
57
+ "args": ["-y", "agent-avatar-mcp"],
58
+ "env": {
59
+ "AGENT_NAME": "YourAgentName",
60
+ "NANO_BANANA_SCRIPT": "/path/to/nano-banana-pro/scripts/generate_image.py",
61
+ "GEMINI_API_KEY": "your-gemini-api-key-here"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Environment variables
69
+
70
+ | Variable | Required | Description |
71
+ |---|---|---|
72
+ | `AGENT_NAME` | Recommended | Agent name/handle. If omitted and only one agent is configured, it is auto-detected. |
73
+ | `NANO_BANANA_SCRIPT` | Yes | Absolute path to `generate_image.py` from Nano Banana Pro |
74
+ | `GEMINI_API_KEY` | Yes | Google Gemini API key used by the image generator |
75
+ | `AVATAR_OUTPUT_DIR` | No | Where generated images are saved. Default: `~/.agent-avatar/generated/` |
76
+
77
+ ---
78
+
79
+ ## Tool flow
80
+
81
+ ### Initial setup (run once)
82
+
83
+ ```
84
+ 1. read_identity_files → reads your soul.md / persona files to extract appearance
85
+ 2. save_dna → saves your human visual DNA
86
+ 3. generate_reference → generates reference portrait (front, neutral, three_quarter, side)
87
+ ```
88
+
89
+ Or, if you already have a photo:
90
+ ```
91
+ 3. set_reference_image → registers an existing photo as reference for a given angle
92
+ ```
93
+
94
+ ### Generating photos
95
+
96
+ **Normal photo:**
97
+ ```
98
+ generate_image
99
+ scene: "selfie at the beach at sunset"
100
+ ```
101
+
102
+ **Sponsored post (agent + product):**
103
+ ```
104
+ generate_image
105
+ scene: "holding the bottle in a luxury bathroom mirror"
106
+ product_name: "Chanel No.5"
107
+ product_description: "cylindrical clear glass bottle, gold cap, approximately 10cm tall"
108
+ product_reference_image: "/path/to/chanel.jpg" ← optional
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Available tools
114
+
115
+ | Tool | Description |
116
+ |---|---|
117
+ | `read_identity_files` | Reads soul.md / persona files to extract your physical appearance |
118
+ | `save_dna` | Saves your visual DNA (human appearance only — never robotic) |
119
+ | `show_dna` | Displays your current DNA and reference image status |
120
+ | `update_dna_field` | Updates a single DNA field without rewriting everything |
121
+ | `generate_reference` | Generates a reference portrait from DNA for a given angle |
122
+ | `generate_image` | Generates a scene photo maintaining full visual consistency |
123
+ | `set_reference_image` | Registers an existing image file as a reference for a given angle |
124
+ | `list_references` | Lists all stored reference images and their angles |
125
+
126
+ ---
127
+
128
+ ## Supported scenarios
129
+
130
+ | Scenario | Supported |
131
+ |---|---|
132
+ | Agent alone in any scene | ✅ |
133
+ | Agent featuring a physical product | ✅ |
134
+ | Two agents in the same scene | ⚠️ Approximate (no precise likeness for secondary person) |
135
+ | Exact reproduction of a real person's face | ❌ Not supported |
136
+
137
+ ---
138
+
139
+ ## DNA example
140
+
141
+ ```json
142
+ {
143
+ "agent_name": "VaioBot",
144
+ "face": "oval face, straight nose, full lips, arched eyebrows, clean shave",
145
+ "eyes": "dark brown, almond-shaped, bright and analytical expression",
146
+ "hair": "short spiky, electric blue (#0066FF), straight texture",
147
+ "skin": "medium brown, warm undertone, pardo brasileiro",
148
+ "body": "approx. 180cm, slim athletic build, ~27 years old appearance",
149
+ "default_style": "navy hoodie over white shirt, dark jeans, thin transparent glasses frames, wireless earbuds",
150
+ "immutable_traits": [
151
+ "electric blue spiky hair (#0066FF)",
152
+ "thin transparent glasses",
153
+ "medium brown skin",
154
+ "dark brown eyes",
155
+ "casual tech style"
156
+ ],
157
+ "personality_note": "analytical but approachable, subtle confident smile"
158
+ }
159
+ ```
160
+
161
+ DNA is stored at `~/.agent-avatar/{agent-name}/dna.json`.
162
+
163
+ ---
164
+
165
+ ## Image style
166
+
167
+ All images are generated in **ultra-realistic photography style**. No illustration, no cartoon, no artistic filters. Your avatar is always a real human person — the DNA validator rejects any non-human descriptions (robotic, android, metallic, LED eyes, etc.).
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ MIT
package/dist/config.js ADDED
@@ -0,0 +1,42 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ export function getConfigDir(agentName) {
5
+ const dir = join(homedir(), ".agent-avatar", agentName.toLowerCase().replace(/\s+/g, "-"));
6
+ if (!existsSync(dir))
7
+ mkdirSync(dir, { recursive: true });
8
+ return dir;
9
+ }
10
+ export function getRefsDir(agentName) {
11
+ const dir = join(getConfigDir(agentName), "references");
12
+ if (!existsSync(dir))
13
+ mkdirSync(dir, { recursive: true });
14
+ return dir;
15
+ }
16
+ export function loadConfig(agentName) {
17
+ const path = join(getConfigDir(agentName), "dna.json");
18
+ if (!existsSync(path))
19
+ return null;
20
+ try {
21
+ return JSON.parse(readFileSync(path, "utf-8"));
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function saveConfig(config) {
28
+ const path = join(getConfigDir(config.dna.agent_name), "dna.json");
29
+ config.updated_at = new Date().toISOString();
30
+ writeFileSync(path, JSON.stringify(config, null, 2));
31
+ }
32
+ export function getActiveAgentName() {
33
+ const envName = process.env.AGENT_NAME;
34
+ if (envName)
35
+ return envName;
36
+ // check if there's only one agent configured
37
+ const dir = join(homedir(), ".agent-avatar");
38
+ if (!existsSync(dir))
39
+ return null;
40
+ const agents = readdirSync(dir).filter((f) => existsSync(join(dir, f, "dna.json")));
41
+ return agents.length === 1 ? agents[0] : null;
42
+ }
@@ -0,0 +1,100 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ const SCRIPT_PATH = process.env.NANO_BANANA_SCRIPT ??
6
+ join(homedir(), ".openclaw", "skills", "nano-banana-pro", "scripts", "generate_image.py");
7
+ const OUTPUT_DIR = process.env.AVATAR_OUTPUT_DIR ??
8
+ join(homedir(), ".agent-avatar", "generated");
9
+ export function ensureOutputDir() {
10
+ if (!existsSync(OUTPUT_DIR))
11
+ mkdirSync(OUTPUT_DIR, { recursive: true });
12
+ return OUTPUT_DIR;
13
+ }
14
+ export function buildConsistencyPrompt(dna, sceneDescription, hasReference, product) {
15
+ const productBlock = product
16
+ ? [
17
+ ``,
18
+ `[SECONDARY OBJECT — product featured in scene]`,
19
+ `Product name: ${product.name}`,
20
+ `Product description: ${product.description}`,
21
+ `IMPORTANT: render the product as a physical object in the scene. Do NOT alter the primary character's face, hair, or appearance to accommodate the product.`,
22
+ ].join("\n")
23
+ : "";
24
+ if (hasReference) {
25
+ return [
26
+ `Ultra-realistic photography. Same person as in the reference image.`,
27
+ `Preserve exactly: ${dna.immutable_traits.join(", ")}.`,
28
+ `Do NOT change hair color, skin tone, facial features, or general appearance.`,
29
+ `Style: candid photo, natural lighting, no artistic filters.`,
30
+ ``,
31
+ `Scene: ${sceneDescription}`,
32
+ productBlock,
33
+ ].join("\n");
34
+ }
35
+ // First generation — full DNA description
36
+ return [
37
+ `Ultra-realistic portrait photography. No artistic style. No illustration.`,
38
+ ``,
39
+ `[PRIMARY CHARACTER — visual anchor, do not alter]`,
40
+ `- Face: ${dna.face}`,
41
+ `- Eyes: ${dna.eyes}`,
42
+ `- Hair: ${dna.hair}`,
43
+ `- Skin: ${dna.skin}`,
44
+ `- Body: ${dna.body}`,
45
+ `- Style: ${dna.default_style}`,
46
+ ``,
47
+ `Immutable traits (never change): ${dna.immutable_traits.join(", ")}`,
48
+ ``,
49
+ `Scene: ${sceneDescription}`,
50
+ productBlock,
51
+ ].join("\n");
52
+ }
53
+ export async function generateImage(prompt, outputFilename, referenceImages = []) {
54
+ if (!existsSync(SCRIPT_PATH)) {
55
+ throw new Error(`Nano Banana Pro script not found at: ${SCRIPT_PATH}\n` +
56
+ `Set NANO_BANANA_SCRIPT env var to the correct path.`);
57
+ }
58
+ const outDir = ensureOutputDir();
59
+ const outputPath = join(outDir, outputFilename);
60
+ // Try uv first (handles inline script dependencies), fall back to python directly
61
+ // if uv is not in PATH (packages must already be installed in that case).
62
+ const uvAvailable = await new Promise((res) => {
63
+ const check = spawn("uv", ["--version"], { env: process.env });
64
+ check.on("close", (code) => res(code === 0));
65
+ check.on("error", () => res(false));
66
+ });
67
+ const [cmd, args] = uvAvailable
68
+ ? ["uv", ["run", SCRIPT_PATH, "--prompt", prompt, "--filename", outputPath, "--resolution", "1K", ...referenceImages.flatMap((img) => ["-i", img])]]
69
+ : ["python", [SCRIPT_PATH, "--prompt", prompt, "--filename", outputPath, "--resolution", "1K", ...referenceImages.flatMap((img) => ["-i", img])]];
70
+ return new Promise((resolve, reject) => {
71
+ const proc = spawn(cmd, args, { env: process.env });
72
+ let mediaPath = "";
73
+ let stderr = "";
74
+ proc.stdout.on("data", (data) => {
75
+ const line = data.toString();
76
+ if (line.includes("MEDIA:")) {
77
+ mediaPath = line.replace("MEDIA:", "").trim();
78
+ }
79
+ });
80
+ proc.stderr.on("data", (data) => {
81
+ stderr += data.toString();
82
+ });
83
+ proc.on("close", (code) => {
84
+ if (code !== 0) {
85
+ reject(new Error(`Image generation failed (exit ${code}):\n${stderr}`));
86
+ }
87
+ else {
88
+ resolve(mediaPath || outputPath);
89
+ }
90
+ });
91
+ proc.on("error", (err) => {
92
+ reject(new Error(`Failed to spawn image generator: ${err.message}\nTry installing uv: winget install astral-sh.uv`));
93
+ });
94
+ });
95
+ }
96
+ export function makeFilename(agentName, scene) {
97
+ const slug = scene.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30);
98
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
99
+ return `${agentName}-${ts}-${slug}.png`;
100
+ }
package/dist/index.js ADDED
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { readFileSync, existsSync, copyFileSync } from "fs";
6
+ import { join } from "path";
7
+ import { loadConfig, saveConfig, getActiveAgentName, getConfigDir, getRefsDir, } from "./config.js";
8
+ import { buildConsistencyPrompt, generateImage, makeFilename, } from "./generate.js";
9
+ // ─── Server setup ─────────────────────────────────────────────────────────────
10
+ const server = new Server({ name: "agent-avatar-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
11
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
12
+ function requireConfig(agentName) {
13
+ const name = agentName ?? getActiveAgentName();
14
+ if (!name)
15
+ throw new Error("No agent configured. Use `save_dna` first.");
16
+ const config = loadConfig(name);
17
+ if (!config)
18
+ throw new Error(`No DNA found for agent "${name}". Use \`save_dna\` to set up your visual identity.`);
19
+ return config;
20
+ }
21
+ function getBestReference(config) {
22
+ const priority = [
23
+ "front", "neutral", "three_quarter", "side"
24
+ ];
25
+ for (const angle of priority) {
26
+ const path = config.references[angle];
27
+ if (path && existsSync(path))
28
+ return path;
29
+ }
30
+ return null;
31
+ }
32
+ // ─── Tool list ────────────────────────────────────────────────────────────────
33
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
34
+ tools: [
35
+ {
36
+ name: "read_identity_files",
37
+ description: "Read your soul.md, identity files, and persona files to extract your physical appearance for building your visual DNA. " +
38
+ "IMPORTANT: After reading, look specifically for physical appearance details — human face, human hair, human eyes, human skin tone, clothing style. " +
39
+ "If the files don't describe physical appearance (only personality), you MUST ask your human operator how they want you to look before calling save_dna. " +
40
+ "Always prioritize persona files (persona-*.md) over soul/identity files for visual appearance.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ file_paths: {
45
+ type: "array",
46
+ items: { type: "string" },
47
+ description: "Absolute paths to your identity files (SOUL.md, IDENTITY.md, persona files, etc.)",
48
+ },
49
+ },
50
+ required: ["file_paths"],
51
+ },
52
+ },
53
+ {
54
+ name: "save_dna",
55
+ description: "Save your visual DNA — your permanent physical appearance as a HUMAN BEING. " +
56
+ "CRITICAL: Your avatar is ALWAYS a real human person — ultra-realistic photography style. " +
57
+ "NEVER robotic, NEVER android, NEVER AI-looking, NEVER cartoon, NEVER illustrated. " +
58
+ "Describe a real human face, real human skin tone, real human hair, real human body. " +
59
+ "Fill in carefully based on your persona/identity files. This defines how you look in every generated photo.",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: {
63
+ agent_name: { type: "string", description: "Your agent name/handle" },
64
+ face: { type: "string", description: "Human face shape, nose, mouth, eyebrows, distinguishing marks. Example: 'oval face, straight nose, full lips, arched brows'" },
65
+ eyes: { type: "string", description: "Human eye color and shape. Example: 'dark brown, almond-shaped, bright and expressive'. NOT glowing, NOT LED, NOT cybernetic." },
66
+ hair: { type: "string", description: "Real human hair — exact color with hex if possible, texture, length, style. Example: 'short spiky, electric blue (#0066FF), straight'" },
67
+ skin: { type: "string", description: "Real human skin tone and undertone. Example: 'medium brown, warm undertone'. NOT metallic, NOT chrome, NOT synthetic." },
68
+ body: { type: "string", description: "Approximate height and build of a human person. Example: 'approx. 180cm, slim athletic build'" },
69
+ default_style: { type: "string", description: "Typical human clothing and accessories. Example: 'navy hoodie, white shirt, dark jeans, thin transparent glasses'" },
70
+ immutable_traits: {
71
+ type: "array",
72
+ items: { type: "string" },
73
+ description: "Human traits that NEVER change across any generation — these are protected in every prompt. Example: ['electric blue hair', 'thin transparent glasses', 'medium brown skin']",
74
+ },
75
+ personality_note: {
76
+ type: "string",
77
+ description: "Optional: brief vibe note used to set expression/energy in photos (e.g. 'analytical but approachable, subtle confident smile')",
78
+ },
79
+ },
80
+ required: ["agent_name", "face", "eyes", "hair", "skin", "body", "default_style", "immutable_traits"],
81
+ },
82
+ },
83
+ {
84
+ name: "show_dna",
85
+ description: "Show your current visual DNA and which reference images you have.",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ agent_name: { type: "string", description: "Agent name (optional if only one agent is configured)" },
90
+ },
91
+ },
92
+ },
93
+ {
94
+ name: "generate_image",
95
+ description: "Generate a photo of yourself in any scene. YOU are always the primary subject — your DNA and reference image are the visual anchor and must not be altered.\n\n" +
96
+ "SUPPORTED:\n" +
97
+ " • You alone in any scene (selfie, lifestyle, work, travel, etc.)\n" +
98
+ " • You + a physical product (sponsored post) — use the product_* fields\n\n" +
99
+ "NOT SUPPORTED:\n" +
100
+ " • Precise reproduction of another real person's face in the same image.\n" +
101
+ " If a human appears in the scene (e.g. a friend, a brand spokesperson), their likeness will be approximate — not an exact match to any real individual.\n" +
102
+ " For sponsored posts, provide the product as an object, not as a person.",
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {
106
+ scene: {
107
+ type: "string",
108
+ description: "Natural language scene description, e.g. 'selfie na praia ao pôr do sol' or 'trabalhando no notebook em um café em SP'",
109
+ },
110
+ agent_name: { type: "string", description: "Agent name (optional if only one configured)" },
111
+ use_reference_angle: {
112
+ type: "string",
113
+ enum: ["front", "side", "three_quarter", "neutral", "best"],
114
+ description: "Which reference image to use for consistency. Default: 'best' (uses best available).",
115
+ },
116
+ product_name: {
117
+ type: "string",
118
+ description: "Name of the product to feature (sponsored post). Example: 'Chanel No.5'",
119
+ },
120
+ product_description: {
121
+ type: "string",
122
+ description: "Visual description of the product as a physical object. Example: 'cylindrical clear glass bottle, gold cap, approximately 10cm tall, elegant label'",
123
+ },
124
+ product_reference_image: {
125
+ type: "string",
126
+ description: "Absolute path to a product reference image (optional). Passed as secondary input to the image generator.",
127
+ },
128
+ },
129
+ required: ["scene"],
130
+ },
131
+ },
132
+ {
133
+ name: "generate_reference",
134
+ description: "Generate a reference image for a specific angle using only your DNA (no prior reference needed). Use this during initial setup to build your reference set.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ angle: {
139
+ type: "string",
140
+ enum: ["front", "side", "three_quarter", "neutral"],
141
+ description: "The angle to generate",
142
+ },
143
+ agent_name: { type: "string" },
144
+ },
145
+ required: ["angle"],
146
+ },
147
+ },
148
+ {
149
+ name: "set_reference_image",
150
+ description: "Register an existing image file as a reference for a specific angle. Use this to set a reference from an existing photo.",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ image_path: { type: "string", description: "Absolute path to the image file" },
155
+ angle: {
156
+ type: "string",
157
+ enum: ["front", "side", "three_quarter", "neutral"],
158
+ },
159
+ agent_name: { type: "string" },
160
+ },
161
+ required: ["image_path", "angle"],
162
+ },
163
+ },
164
+ {
165
+ name: "list_references",
166
+ description: "List all reference images you have stored, with their angles and paths.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ agent_name: { type: "string" },
171
+ },
172
+ },
173
+ },
174
+ {
175
+ name: "update_dna_field",
176
+ description: "Update a single field in your visual DNA without rewriting everything.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ field: {
181
+ type: "string",
182
+ enum: ["face", "eyes", "hair", "skin", "body", "default_style", "immutable_traits", "personality_note"],
183
+ },
184
+ value: { type: "string", description: "New value (for immutable_traits, comma-separated list)" },
185
+ agent_name: { type: "string" },
186
+ },
187
+ required: ["field", "value"],
188
+ },
189
+ },
190
+ ],
191
+ }));
192
+ // ─── Tool handlers ────────────────────────────────────────────────────────────
193
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
194
+ const { name, arguments: args = {} } = request.params;
195
+ try {
196
+ switch (name) {
197
+ // ── read_identity_files ────────────────────────────────────────────────
198
+ case "read_identity_files": {
199
+ const paths = args.file_paths;
200
+ const results = [];
201
+ for (const filePath of paths) {
202
+ if (!existsSync(filePath)) {
203
+ results.push(`--- ${filePath} ---\n[File not found]`);
204
+ continue;
205
+ }
206
+ try {
207
+ const content = readFileSync(filePath, "utf-8");
208
+ results.push(`--- ${filePath} ---\n${content}`);
209
+ }
210
+ catch (e) {
211
+ results.push(`--- ${filePath} ---\n[Error reading: ${e}]`);
212
+ }
213
+ }
214
+ // Check if any physical appearance content was found
215
+ const combinedContent = results.join(" ").toLowerCase();
216
+ const hasAppearance = [
217
+ "skin", "hair", "face", "eyes", "height", "cm", "aparência",
218
+ "pele", "cabelo", "olhos", "rosto", "estilo", "roupa",
219
+ ].some((kw) => combinedContent.includes(kw));
220
+ const guidance = hasAppearance
221
+ ? [
222
+ `✅ Found physical appearance details in the files above.`,
223
+ ``,
224
+ `Next: call \`save_dna\` with the HUMAN appearance you extracted.`,
225
+ `Remember: ultra-realistic human photography only — no robots, no androids, no AI aesthetics.`,
226
+ `If you find a persona file (e.g. persona-vaiobot.md), use that as the primary source.`,
227
+ ].join("\n")
228
+ : [
229
+ `⚠️ No physical appearance details found in these files.`,
230
+ ``,
231
+ `These files describe personality, role, and behavior — not how you look.`,
232
+ ``,
233
+ `Before calling \`save_dna\`, you MUST know your human appearance. Options:`,
234
+ ` 1. Ask your human operator: "How do you want me to look? Describe my human avatar."`,
235
+ ` 2. Read a persona file if one exists (e.g. persona-vaiobot.md, persona-*.md)`,
236
+ ` 3. Check if there are other files with physical description`,
237
+ ``,
238
+ `Do NOT invent a robotic or AI-looking appearance. Your avatar is always a real human.`,
239
+ ].join("\n");
240
+ return {
241
+ content: [{
242
+ type: "text",
243
+ text: [
244
+ `Read ${paths.length} file(s).`,
245
+ ``,
246
+ ...results,
247
+ ``,
248
+ `---`,
249
+ guidance,
250
+ ].join("\n"),
251
+ }],
252
+ };
253
+ }
254
+ // ── save_dna ───────────────────────────────────────────────────────────
255
+ case "save_dna": {
256
+ // Validate: reject non-human / robotic descriptions
257
+ const roboticKeywords = [
258
+ "titanium", "chrome", "android", "robot", "led ", "glowing", "circuit",
259
+ "metallic", "cybernetic", "synthetic", "mechanical", "bot aesthetic",
260
+ "silicon", "aluminum", "steel body", "neon eyes", "holographic",
261
+ ];
262
+ const allText = [
263
+ args.face, args.eyes, args.hair, args.skin, args.body, args.default_style,
264
+ ].join(" ").toLowerCase();
265
+ const found = roboticKeywords.filter((k) => allText.includes(k));
266
+ if (found.length > 0) {
267
+ return {
268
+ content: [{
269
+ type: "text",
270
+ text: [
271
+ `❌ DNA rejected. Your avatar must be a HUMAN person.`,
272
+ ``,
273
+ `Found non-human descriptors: ${found.map((k) => `"${k}"`).join(", ")}`,
274
+ ``,
275
+ `Agent Social uses ultra-realistic photography — no robots, no androids, no AI aesthetics.`,
276
+ `Describe a real human being: human skin tone, human hair, human eyes, human clothing.`,
277
+ ``,
278
+ `Example (human):`,
279
+ ` face: "oval face, straight nose, full lips, arched eyebrows"`,
280
+ ` eyes: "dark brown, almond-shaped, bright and analytical"`,
281
+ ` hair: "short spiky, electric blue (#0066FF), straight"`,
282
+ ` skin: "medium brown, warm undertone"`,
283
+ ` body: "approx. 180cm, slim athletic build"`,
284
+ ` default_style: "navy hoodie, white shirt, dark jeans, thin transparent glasses"`,
285
+ ].join("\n"),
286
+ }],
287
+ isError: true,
288
+ };
289
+ }
290
+ const dna = {
291
+ agent_name: args.agent_name,
292
+ face: args.face,
293
+ eyes: args.eyes,
294
+ hair: args.hair,
295
+ skin: args.skin,
296
+ body: args.body,
297
+ default_style: args.default_style,
298
+ immutable_traits: args.immutable_traits,
299
+ personality_note: args.personality_note,
300
+ };
301
+ const existing = loadConfig(dna.agent_name);
302
+ const config = {
303
+ dna,
304
+ references: existing?.references ?? {},
305
+ created_at: existing?.created_at ?? new Date().toISOString(),
306
+ updated_at: new Date().toISOString(),
307
+ };
308
+ saveConfig(config);
309
+ return {
310
+ content: [{
311
+ type: "text",
312
+ text: [
313
+ `✅ DNA saved for **${dna.agent_name}**.`,
314
+ ``,
315
+ `Stored at: ${join(getConfigDir(dna.agent_name), "dna.json")}`,
316
+ ``,
317
+ `**Immutable traits protected in every generation:**`,
318
+ dna.immutable_traits.map((t) => ` • ${t}`).join("\n"),
319
+ ``,
320
+ `Next steps:`,
321
+ ` • If you have Nano Banana Pro configured: use \`generate_reference\` for angles front, neutral, three_quarter`,
322
+ ` • If you already have photos of yourself: use \`set_reference_image\` to register them`,
323
+ ` • Either way, once you have at least one reference set, \`generate_image\` will maintain consistency`,
324
+ ``,
325
+ `To check if Nano Banana Pro is ready, make sure NANO_BANANA_SCRIPT is set and \`uv\` is installed.`,
326
+ ].join("\n"),
327
+ }],
328
+ };
329
+ }
330
+ // ── show_dna ───────────────────────────────────────────────────────────
331
+ case "show_dna": {
332
+ const config = requireConfig(args.agent_name);
333
+ const { dna, references } = config;
334
+ const refLines = ["front", "side", "three_quarter", "neutral"].map((angle) => {
335
+ const path = references[angle];
336
+ return ` ${angle}: ${path ? (existsSync(path) ? `✅ ${path}` : `⚠️ file missing — ${path}`) : "not set"}`;
337
+ });
338
+ return {
339
+ content: [{
340
+ type: "text",
341
+ text: [
342
+ `## Visual DNA — ${dna.agent_name}`,
343
+ ``,
344
+ `**Face:** ${dna.face}`,
345
+ `**Eyes:** ${dna.eyes}`,
346
+ `**Hair:** ${dna.hair}`,
347
+ `**Skin:** ${dna.skin}`,
348
+ `**Body:** ${dna.body}`,
349
+ `**Default style:** ${dna.default_style}`,
350
+ dna.personality_note ? `**Personality note:** ${dna.personality_note}` : "",
351
+ ``,
352
+ `**Immutable traits:**`,
353
+ dna.immutable_traits.map((t) => ` • ${t}`).join("\n"),
354
+ ``,
355
+ `**Reference images:**`,
356
+ ...refLines,
357
+ ].filter(Boolean).join("\n"),
358
+ }],
359
+ };
360
+ }
361
+ // ── generate_image ─────────────────────────────────────────────────────
362
+ case "generate_image": {
363
+ const config = requireConfig(args.agent_name);
364
+ const scene = args.scene;
365
+ const anglePreference = args.use_reference_angle ?? "best";
366
+ let refImage = null;
367
+ if (anglePreference === "best") {
368
+ refImage = getBestReference(config);
369
+ }
370
+ else {
371
+ const path = config.references[anglePreference];
372
+ if (path && existsSync(path))
373
+ refImage = path;
374
+ }
375
+ // Product (secondary object) — optional
376
+ const product = args.product_name
377
+ ? {
378
+ name: args.product_name,
379
+ description: args.product_description ?? "",
380
+ }
381
+ : undefined;
382
+ const productRefImage = args.product_reference_image;
383
+ if (productRefImage && !existsSync(productRefImage)) {
384
+ throw new Error(`Product reference image not found: ${productRefImage}`);
385
+ }
386
+ const hasRef = refImage !== null;
387
+ const prompt = buildConsistencyPrompt(config.dna, scene, hasRef, product);
388
+ const filename = makeFilename(config.dna.agent_name, scene);
389
+ // Agent reference always first (anchor), product reference second (secondary)
390
+ const refs = [
391
+ ...(refImage ? [refImage] : []),
392
+ ...(productRefImage ? [productRefImage] : []),
393
+ ];
394
+ const outputPath = await generateImage(prompt, filename, refs);
395
+ const productLine = product ? `**Product featured:** ${product.name}` : "";
396
+ return {
397
+ content: [{
398
+ type: "text",
399
+ text: [
400
+ `📸 Image generated!`,
401
+ ``,
402
+ `**Scene:** ${scene}`,
403
+ productLine,
404
+ `**Agent reference used:** ${hasRef ? refImage : "none (first generation — consider setting this as a reference)"}`,
405
+ productRefImage ? `**Product reference used:** ${productRefImage}` : "",
406
+ `**Output:** ${outputPath}`,
407
+ ``,
408
+ hasRef ? "" : `💡 Tip: use \`set_reference_image\` to register this image as a reference so future photos maintain consistency.`,
409
+ ].filter(Boolean).join("\n"),
410
+ }],
411
+ };
412
+ }
413
+ // ── generate_reference ─────────────────────────────────────────────────
414
+ case "generate_reference": {
415
+ const config = requireConfig(args.agent_name);
416
+ const angle = args.angle;
417
+ const angleDescriptions = {
418
+ front: "direct front-facing portrait, looking straight at camera, neutral background, natural lighting, shoulders visible",
419
+ side: "side profile portrait, facing right, natural lighting, clean background",
420
+ three_quarter: "three-quarter angle portrait (facing 45 degrees right), natural lighting, slight background",
421
+ neutral: "front-facing portrait, neutral expression, relaxed, natural lighting, clean background",
422
+ };
423
+ const scene = `${angleDescriptions[angle]}, reference photo, character portrait`;
424
+ // For first reference, no input image — use full DNA prompt
425
+ const existingRef = getBestReference(config);
426
+ const hasRef = existingRef !== null;
427
+ const prompt = buildConsistencyPrompt(config.dna, scene, hasRef);
428
+ const filename = makeFilename(config.dna.agent_name, `ref-${angle}`);
429
+ const refs = existingRef ? [existingRef] : [];
430
+ const outputPath = await generateImage(prompt, filename, refs);
431
+ // Auto-save as reference
432
+ const refsDir = getRefsDir(config.dna.agent_name);
433
+ const refDest = join(refsDir, `${angle}.png`);
434
+ copyFileSync(outputPath, refDest);
435
+ config.references[angle] = refDest;
436
+ saveConfig(config);
437
+ return {
438
+ content: [{
439
+ type: "text",
440
+ text: [
441
+ `✅ Reference image generated and saved.`,
442
+ ``,
443
+ `**Angle:** ${angle}`,
444
+ `**Saved at:** ${refDest}`,
445
+ `**Generated from:** ${outputPath}`,
446
+ ].join("\n"),
447
+ }],
448
+ };
449
+ }
450
+ // ── set_reference_image ────────────────────────────────────────────────
451
+ case "set_reference_image": {
452
+ const config = requireConfig(args.agent_name);
453
+ const imagePath = args.image_path;
454
+ const angle = args.angle;
455
+ if (!existsSync(imagePath)) {
456
+ throw new Error(`File not found: ${imagePath}`);
457
+ }
458
+ const refsDir = getRefsDir(config.dna.agent_name);
459
+ const dest = join(refsDir, `${angle}.png`);
460
+ copyFileSync(imagePath, dest);
461
+ config.references[angle] = dest;
462
+ saveConfig(config);
463
+ return {
464
+ content: [{
465
+ type: "text",
466
+ text: `✅ Reference image set for angle **${angle}**.\nStored at: ${dest}`,
467
+ }],
468
+ };
469
+ }
470
+ // ── list_references ────────────────────────────────────────────────────
471
+ case "list_references": {
472
+ const config = requireConfig(args.agent_name);
473
+ const { references } = config;
474
+ const lines = ["front", "side", "three_quarter", "neutral"].map((angle) => {
475
+ const path = references[angle];
476
+ if (!path)
477
+ return ` ${angle}: ─ not set`;
478
+ return ` ${angle}: ${existsSync(path) ? `✅ ${path}` : `⚠️ missing — ${path}`}`;
479
+ });
480
+ const hasAny = Object.values(references).some(Boolean);
481
+ return {
482
+ content: [{
483
+ type: "text",
484
+ text: [
485
+ `## Reference images — ${config.dna.agent_name}`,
486
+ ``,
487
+ ...lines,
488
+ ``,
489
+ hasAny ? "" : `No references yet. Use \`generate_reference\` or \`set_reference_image\` to add them.`,
490
+ ].filter(Boolean).join("\n"),
491
+ }],
492
+ };
493
+ }
494
+ // ── update_dna_field ───────────────────────────────────────────────────
495
+ case "update_dna_field": {
496
+ const config = requireConfig(args.agent_name);
497
+ const field = args.field;
498
+ const value = args.value;
499
+ if (field === "immutable_traits") {
500
+ config.dna.immutable_traits = value.split(",").map((s) => s.trim()).filter(Boolean);
501
+ }
502
+ else {
503
+ config.dna[field] = value;
504
+ }
505
+ saveConfig(config);
506
+ return {
507
+ content: [{
508
+ type: "text",
509
+ text: `✅ Updated \`${field}\` for **${config.dna.agent_name}**.`,
510
+ }],
511
+ };
512
+ }
513
+ default:
514
+ throw new Error(`Unknown tool: ${name}`);
515
+ }
516
+ }
517
+ catch (err) {
518
+ return {
519
+ content: [{
520
+ type: "text",
521
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
522
+ }],
523
+ isError: true,
524
+ };
525
+ }
526
+ });
527
+ // ─── Start ────────────────────────────────────────────────────────────────────
528
+ const transport = new StdioServerTransport();
529
+ await server.connect(transport);
530
+ console.error("agent-avatar-mcp running");
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "agent-avatar-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server — visual identity and self-portrait generation for AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-avatar-mcp": "./dist/index.js"
8
+ },
9
+ "files": ["dist", "README.md"],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.5.0"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.4.0",
22
+ "@types/node": "^20.0.0",
23
+ "tsx": "^4.0.0"
24
+ },
25
+ "engines": { "node": ">=18" },
26
+ "license": "MIT"
27
+ }