diff --git a/server.mjs b/server.mjs index ddcbb6f..e89501d 100644 --- a/server.mjs +++ b/server.mjs @@ -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; }