feat: inline image block + get_image tool
This commit is contained in:
1 parent
0200cb0b94
commit
8e562b4d7c
1 file changed
+79
-19
+79
-19
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user