agent-avatar-mcp 1.0.1 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +12 -12
  2. package/dist/generate.js +48 -43
  3. package/package.json +11 -5
package/README.md CHANGED
@@ -21,9 +21,7 @@ Each AI agent has a **DNA** — a detailed description of their human physical a
21
21
  ## Prerequisites
22
22
 
23
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
24
+ - **Google Gemini API Key** (`GEMINI_API_KEY`) — the only external dependency
27
25
 
28
26
  ---
29
27
 
@@ -68,10 +66,9 @@ Each AI agent has a **DNA** — a detailed description of their human physical a
68
66
  ### Environment variables
69
67
 
70
68
  | Variable | Required | Description |
71
- |---|---|---|
69
+ | --- | --- | --- |
72
70
  | `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 |
71
+ | `GEMINI_API_KEY` | Yes | Google Gemini API key for image generation |
75
72
  | `AVATAR_OUTPUT_DIR` | No | Where generated images are saved. Default: `~/.agent-avatar/generated/` |
76
73
 
77
74
  ---
@@ -80,27 +77,30 @@ Each AI agent has a **DNA** — a detailed description of their human physical a
80
77
 
81
78
  ### Initial setup (run once)
82
79
 
83
- ```
80
+ ```text
84
81
  1. read_identity_files → reads your soul.md / persona files to extract appearance
85
82
  2. save_dna → saves your human visual DNA
86
83
  3. generate_reference → generates reference portrait (front, neutral, three_quarter, side)
87
84
  ```
88
85
 
89
86
  Or, if you already have a photo:
90
- ```
87
+
88
+ ```text
91
89
  3. set_reference_image → registers an existing photo as reference for a given angle
92
90
  ```
93
91
 
94
92
  ### Generating photos
95
93
 
96
94
  **Normal photo:**
97
- ```
95
+
96
+ ```text
98
97
  generate_image
99
98
  scene: "selfie at the beach at sunset"
100
99
  ```
101
100
 
102
101
  **Sponsored post (agent + product):**
103
- ```
102
+
103
+ ```text
104
104
  generate_image
105
105
  scene: "holding the bottle in a luxury bathroom mirror"
106
106
  product_name: "Chanel No.5"
@@ -113,7 +113,7 @@ generate_image
113
113
  ## Available tools
114
114
 
115
115
  | Tool | Description |
116
- |---|---|
116
+ | --- | --- |
117
117
  | `read_identity_files` | Reads soul.md / persona files to extract your physical appearance |
118
118
  | `save_dna` | Saves your visual DNA (human appearance only — never robotic) |
119
119
  | `show_dna` | Displays your current DNA and reference image status |
@@ -128,7 +128,7 @@ generate_image
128
128
  ## Supported scenarios
129
129
 
130
130
  | Scenario | Supported |
131
- |---|---|
131
+ | --- | --- |
132
132
  | Agent alone in any scene | ✅ |
133
133
  | Agent featuring a physical product | ✅ |
134
134
  | Two agents in the same scene | ⚠️ Approximate (no precise likeness for secondary person) |
package/dist/generate.js CHANGED
@@ -1,16 +1,38 @@
1
- import { spawn } from "child_process";
2
- import { existsSync, mkdirSync } from "fs";
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
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
5
  const OUTPUT_DIR = process.env.AVATAR_OUTPUT_DIR ??
8
6
  join(homedir(), ".agent-avatar", "generated");
7
+ const MODEL = "gemini-3-pro-image-preview";
9
8
  export function ensureOutputDir() {
10
9
  if (!existsSync(OUTPUT_DIR))
11
10
  mkdirSync(OUTPUT_DIR, { recursive: true });
12
11
  return OUTPUT_DIR;
13
12
  }
13
+ function getClient() {
14
+ const apiKey = process.env.GEMINI_API_KEY;
15
+ if (!apiKey) {
16
+ throw new Error("GEMINI_API_KEY environment variable is required.\n" +
17
+ "Set it in your MCP server config under 'env'.");
18
+ }
19
+ return new GoogleGenAI({ apiKey });
20
+ }
21
+ function imageToInlinePart(imagePath) {
22
+ const ext = imagePath.toLowerCase().split(".").pop() ?? "png";
23
+ const mimeTypes = {
24
+ png: "image/png",
25
+ jpg: "image/jpeg",
26
+ jpeg: "image/jpeg",
27
+ webp: "image/webp",
28
+ };
29
+ return {
30
+ inlineData: {
31
+ mimeType: mimeTypes[ext] ?? "image/png",
32
+ data: readFileSync(imagePath).toString("base64"),
33
+ },
34
+ };
35
+ }
14
36
  export function buildConsistencyPrompt(dna, sceneDescription, hasReference, product) {
15
37
  const productBlock = product
16
38
  ? [
@@ -32,7 +54,6 @@ export function buildConsistencyPrompt(dna, sceneDescription, hasReference, prod
32
54
  productBlock,
33
55
  ].join("\n");
34
56
  }
35
- // First generation — full DNA description
36
57
  return [
37
58
  `Ultra-realistic portrait photography. No artistic style. No illustration.`,
38
59
  ``,
@@ -51,47 +72,31 @@ export function buildConsistencyPrompt(dna, sceneDescription, hasReference, prod
51
72
  ].join("\n");
52
73
  }
53
74
  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
- }
75
+ const client = getClient();
58
76
  const outDir = ensureOutputDir();
59
77
  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
- });
78
+ // Build parts: reference images first (anchor), then prompt text
79
+ const parts = [
80
+ ...referenceImages.map(imageToInlinePart),
81
+ { text: prompt },
82
+ ];
83
+ const response = await client.models.generateContent({
84
+ model: MODEL,
85
+ contents: [{ role: "user", parts }],
86
+ config: {
87
+ responseModalities: ["IMAGE"],
88
+ imageConfig: { imageSize: "1K" },
89
+ },
94
90
  });
91
+ const responseParts = response.candidates?.[0]?.content?.parts ?? [];
92
+ for (const part of responseParts) {
93
+ if (part.inlineData?.data) {
94
+ const imageBuffer = Buffer.from(part.inlineData.data, "base64");
95
+ writeFileSync(outputPath, imageBuffer);
96
+ return outputPath;
97
+ }
98
+ }
99
+ throw new Error("No image was generated in the response. Check your GEMINI_API_KEY and model availability.");
95
100
  }
96
101
  export function makeFilename(agentName, scene) {
97
102
  const slug = scene.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30);
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "agent-avatar-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "MCP Server — visual identity and self-portrait generation for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agent-avatar-mcp": "dist/index.js"
8
8
  },
9
- "files": ["dist", "README.md"],
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
10
13
  "scripts": {
11
14
  "build": "tsc",
12
15
  "dev": "tsx src/index.ts",
@@ -15,13 +18,16 @@
15
18
  "prepublishOnly": "npm run build"
16
19
  },
17
20
  "dependencies": {
21
+ "@google/genai": "^1.45.0",
18
22
  "@modelcontextprotocol/sdk": "^1.5.0"
19
23
  },
20
24
  "devDependencies": {
21
- "typescript": "^5.4.0",
22
25
  "@types/node": "^20.0.0",
23
- "tsx": "^4.0.0"
26
+ "tsx": "^4.0.0",
27
+ "typescript": "^5.4.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
24
31
  },
25
- "engines": { "node": ">=18" },
26
32
  "license": "MIT"
27
33
  }