Genace

MCP image generation: build a local server that preserves job IDs

·

An MCP image generation server can make a small wrapper available to several coding assistants. Genace’s REST URL is not itself an MCP endpoint. You run a custom local server that translates tool calls into authenticated HTTP requests. Keep one paid submission tool and one observation tool so clients can display and control those operations independently.

1. Create the local Python server

The following example follows the Python MCP SDK 2 interface documented in the linked MCP guide. Use a Python environment with mcp>=2,<3 and requests, and save the code as genace_mcp.py. Older SDK releases use a different server import; pin and review the version rather than mixing incompatible examples.

python -m pip install 'mcp>=2,<3' requests
import os
import re
import requests
from mcp.server import MCPServer

server = MCPServer("genace-local")

def request(path: str, payload=None) -> dict:
    response = requests.request(
        "POST" if payload is not None else "GET",
        f"https://genace.ai/api/v1{path}",
        headers={"Authorization": f"Bearer {os.environ['GENACE_API_KEY']}"},
        json=payload, timeout=45,
    )
    if payload is not None and response.status_code == 502:
        try:
            body = response.json()
        except ValueError:
            body = {}
        error = body.get("error") if isinstance(body, dict) else None
        if isinstance(error, dict):
            outcomes = {
                "UPSTREAM_SUBMISSION_UNCERTAIN": "uncertain",
                "UPSTREAM_SUBMISSION_FAILED": "rejected",
            }
            outcome = outcomes.get(error.get("code"))
            job_id = error.get("job_id")
            if outcome and isinstance(job_id, str) and job_id.strip():
                return {"submission_outcome": outcome, "job_id": job_id,
                        "error_code": error["code"]}
    response.raise_for_status()
    return response.json()

@server.tool()
def submit_image(prompt: str) -> dict:
    """Submit one paid image generation; obtain authorization before invoking."""
    prompt = prompt.strip()
    if not prompt or len(prompt) > 2000:
        raise ValueError("Prompt must contain 1-2000 characters")
    accepted = request("/images/generations", {
        "model": "nano-banana", "prompt": prompt, "n": 1,
    })
    if "submission_outcome" in accepted:
        return accepted
    return {"job_id": accepted["job_id"], "state": accepted["state"]}

@server.tool()
def inspect_job(job_id: str) -> dict:
    """Inspect one existing generation job without creating another job."""
    if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", job_id):
        raise ValueError("Invalid job identifier")
    job = request(f"/jobs/{job_id}?wait=0")
    return {"job_id": job["id"], "state": job["state"],
            "outputs": job.get("outputs"), "error": job.get("error")}

if __name__ == "__main__":
    server.run(transport="stdio")

2. Configure the host process

Set GENACE_API_KEY in the environment of the process that launches the server. An editor started earlier may not inherit variables changed in a later terminal session. Use the absolute path to the Python interpreter inside your environment and the absolute path to the server file when registering it with an MCP client.

This example is a local single-account wrapper. Before adapting it for several users, add application authentication, operation ownership, and spending controls. A tool description is useful context for the assistant but does not itself enforce authorization.

3. Keep tool results small and actionable

Two recognized submission 502 responses can still provide a local ID at error.job_id. The helper reads those bodies before raising for HTTP errors, so submit_image returns a small recovery record instead of losing that reference from its normal tool result. It forwards neither raw error messages nor credentials. Generic errors and responses without a usable ID still raise; the wrapper does not invent a recovery handle.

submission_outcome is your wrapper’s label, not an additional Genace job state. uncertain means retain the ID and held credits for reconciliation, with no automatic POST retry or cancellation to force a refund. rejected means the submission was explicitly refused, not that refund settlement is already complete. GET failures remain observation failures, even if their body contains one of the submission error codes. See submission uncertainty for the gateway boundary.

A normally returned MCP tool result is not proof that generation was accepted or completed. Have the host branch on the returned outcome and enforce spending authorization in application code; tool descriptions alone do not prevent a model from repeating creation. A disconnected POST with no body still raises a transport exception. Keep the operation context and investigate rather than letting an error-recovery loop call submit_image again.

The submission tool returns a job ID as soon as the REST submission succeeds. Save that ID outside the chat if the workflow must survive lost context. The status tool makes one immediate observation and allows null outputs. It does not spin until completion or trigger another generation when the previous job is pending.

On succeeded, select the output type expected by your consumer. On failed or canceled, preserve the terminal error or state. A successful HTTP response from the status endpoint can still represent a failed generation, so the assistant needs the state field to explain the outcome correctly.

Common mistakes and verification

Do not print ordinary logs to stdout in a stdio server because that channel carries the MCP protocol. Use stderr logging if you add diagnostics. Do not put the secret in tool arguments, return objects, or error traces sent to the client.

First test server startup and tool discovery without creating an image. Use a mocked HTTP layer to verify prompt rejection, known-job inspection, null outputs, and terminal errors. Only then invoke a deliberate live submission. A lost POST response is an uncertain outcome; repeating the tool can create another job because the API exposes no request idempotency contract.

Before live work, inject fixtures for 202, both known 502 outcomes with IDs, a generic 502, a missing recovery ID, and a lost response. Assert one HTTP call per invocation, no secret or raw error detail in recovery results, and no POST when inspecting a known ID. Exercise all five actual job states with null outputs. Offline tests with a stub server decorator verify these Python branches, not real SDK startup, MCP discovery, or a client’s authorization behavior; check those separately on your pinned installation without invoking generation.

See the MCP server guide, Claude Code setup, and Cursor setup for client configuration.

Continue with agents and automation

Models covered on this page

Where these facts come from