feat: inline image block + get_image tool

This commit is contained in:
hermes committed 2026-07-09 19:21:39 +02:00
1 parent 0200cb0b94
commit 8e562b4d7c
1 file changed
+79 -19
+79 -19
View File
@@ -348,7 +348,7 @@ async function saveImage({ dataUrl, model, prefix }) {
// ---------- MCP server ----------
const server = new Server(
{ name: 'mcp-img-gen', version: '1.1.0' },
{ name: 'mcp-img-gen', version: '1.2.0' },
{ capabilities: { tools: {} } },
);
@@ -434,6 +434,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
description: 'List 35 image-capable models on OpenRouter (cached 1h).',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'get_image',
description:
'Read a previously generated image by filename. Returns multipart content: ' +
'one inline image (base64) + one text metadata block. Use this if you want ' +
'to re-fetch an image after a generate_image call (file persists across ' +
'the same container lifetime but is lost on container restart). ' +
'Filename is the value returned in the generate_image metadata.',
inputSchema: {
type: 'object',
properties: {
filename: {
type: 'string',
description:
'Filename as returned in `filename` field from generate_image / ' +
'generate_image_transparent output (e.g. ' +
'"opaque-google_gemini-2.5-flash-image-1783615606608.png").',
},
},
required: ['filename'],
},
},
],
};
});
@@ -491,27 +513,65 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
process.stderr.write(
`[gen] OK provider=${provider} mime=${mime} ${Date.now() - t0}ms ${filepath} ${bytes}b\n`,
);
// Return multipart content: image inline (base64) + text metadata.
// The image block lets MCP clients (Cursor, Claude Desktop, Figma) render
// the picture directly in the chat without an extra tool call.
// The text block exposes structured metadata for the agent / user.
const meta = {
tool: name,
model,
provider,
mime,
aspect,
transparent,
refs_used: imageUrls.length,
filepath,
filename,
bytes,
latency_ms: Date.now() - t0,
};
return {
content: [
{
type: 'image',
mimeType: mime,
data: bytes.toString('base64'),
},
{
type: 'text',
text: JSON.stringify(
{
tool: name,
model,
provider,
mime,
aspect,
transparent,
refs_used: imageUrls.length,
filepath,
filename,
bytes,
latency_ms: Date.now() - t0,
},
null,
2,
),
text: JSON.stringify(meta, null, 2),
},
],
};
}
if (name === 'get_image') {
const filename = String(args?.filename || '').trim();
if (!filename) return errResp('filename required');
// Security: prevent path escape — only flat filenames in OUTPUT_DIR.
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
return errResp('filename must be a flat name (no path separators)');
}
const filepath = path.join(OUTPUT_DIR, filename);
let buf;
try {
buf = await fs.readFile(filepath);
} catch (e) {
return errResp(`Image not found: ${filename} (container may have restarted)`);
}
const mime = filename.endsWith('.png')
? 'image/png'
: filename.endsWith('.jpg') || filename.endsWith('.jpeg')
? 'image/jpeg'
: filename.endsWith('.webp')
? 'image/webp'
: 'application/octet-stream';
process.stderr.write(`[get] OK ${filename} ${buf.length}b\n`);
return {
content: [
{ type: 'image', mimeType: mime, data: buf.toString('base64') },
{
type: 'text',
text: JSON.stringify({ filename, filepath, bytes: buf.length, mime }, null, 2),
},
],
};
@@ -553,7 +613,7 @@ const httpServer = http.createServer(async (req, res) => {
if (req.url === '/health') {
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ ok: true, version: '1.1.0', uptime: process.uptime() }));
res.end(JSON.stringify({ ok: true, version: '1.2.0', uptime: process.uptime() }));
return;
}