DEV Community

Abhishek Banerjee
Abhishek Banerjee

Posted on Originally published at Medium on

Multimodal Generation & Deterministic Tool Calling with Gemini 3 Pro (Nano Banana Pro)

How structured JSON schemas, FastMCP tool wrappers, and GCS binary offloading bridge language models to studio-grade image pipelines.

In early AI agent design, multimodal generation was treated as an out-of-band workflow. A language model generated a text prompt, passed it through a loose string format to an external latent diffusion model, and hoped the resulting image reflected the agent’s internal state.

This decoupling created severe integration friction: spatial hallucinations, loss of camera control, distorted text rendering, and zero guarantee that parameters (e.g., bounding boxes, lighting vectors, or brand color palettes) would be respected.

The arrival of multimodal reasoning architectures specifically Gemini 3 Pro Image (popularly known as Nano Banana Pro) changes this paradigm. By integrating physical reasoning and spatial awareness directly into the model’s token processing, Gemini 3 Pro can generate text, evaluate spatial layouts, and output deterministic JSON payloads designed to trigger specialized media generation microservices via standard Model Context Protocol (MCP) wrappers.

Here is an operational deep dive into how multimodal function calling operates, how standard text models govern latent generation pipelines, and how to resolve the systems friction of payload grounding and heavy binary transport.

1. Under the Hood: Function Calling Mechanics in Multimodal LLMs

When an LLM executes a tool call, it does not execute code directly. Instead, it completes a token generation sequence constrained by an enforced JSON Schema definition provided during system initialization.

The Generation Pipeline Step-by-Step

  1. Schema Injection: The agent host registers tool capabilities via FastMCP. The tool definition declares required arguments aspect ratios, lighting modes, bounded object vectors, and camera angles.
  2. Constrained Decoding (Grammar-Based Sampling): During token generation, the LLM’s logit biases are constrained so that output tokens strictly adhere to valid JSON syntax matching the registered schema.
  3. Parameter Dispatch: The host application catches the completed JSON object, validates it using Pydantic, and routes it to the target latent pipeline (e.g., Nano Banana Pro’s rendering engine).

Structured Tool Call Schema

{
  "name": "nano_banana_generate_image",
  "arguments": {
    "prompt": "An architectural blueprint of a distributed GPU cluster, isometric perspective, dark background.",
    "aspect_ratio": "16:9",
    "rendering_specs": {
      "resolution": "4K",
      "style_preset": "technical_schematic",
      "lighting": "volumetric_blue_neon"
    },
    "spatial_bounds": [
      {
        "label": "central_processing_node",
        "box_2d": [250, 400, 750, 600]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

2. FastMCP Wrappers: Bridging Text Models to Latent Pipelines

The FastMCP framework simplifies creating Python-native MCP tools. It transforms standard typed Python functions into tool specifications that any MCP client (such as Gemini CLI or custom Google ADK runtimes) can inspect and invoke.

By wrapping the Nano Banana Pro generation API in FastMCP, we convert raw image generation parameters into an agent tool primitive.

[Agent System Prompt] ──> Emits JSON-RPC ──> [FastMCP Stdio Server] ──> [Nano Banana Engine]

FastMCP Tool Definition

from fastmcp import FastMCP
from pydantic import BaseModel, Field
import os

mcp = FastMCP("NanoBananaPro-Image-Engine")

class ImageGenerationSchema(BaseModel):
    prompt: str = Field(description="Detailed visual prompt describing the scene.")
    aspect_ratio: str = Field(default="16:9", description="Output aspect ratio (16:9, 1:1, 9:16).")
    resolution: str = Field(default="2K", description="Target resolution: 1K, 2K, or 4K.")
    style_preset: str = Field(default="cinematic", description="Visual aesthetic style.")

@mcp.tool()
async def generate_visual_asset(spec: ImageGenerationSchema) -> str:
    """Generates studio-grade images using the Nano Banana Pro engine."""
    # API invocation logic to Nano Banana Pro backend
    image_uri = await invoke_nano_banana_api(
        prompt=spec.prompt,
        ratio=spec.aspect_ratio,
        res=spec.resolution,
        style=spec.style_preset
    )
    return f"Image successfully generated and stored at: {image_uri}"
Enter fullscreen mode Exit fullscreen mode

3. Systems Friction: Parameter Grounding & Payload Transport

Integrating multimodal generation tools into production agent runtimes introduces two major systems hurdles: Parameter Hallucination and Binary Serialization Overhead.

Problem 1: Parameter Hallucination [Model Output] ──> Generates Invalid Box: [1200, 400, 300, 100] ──> Pipeline Crash! (X2 < X1 Error) Problem 2: Binary Payload Overhead [Raw Base64 Payload] ──> ~10 MB Text Stream ──> Context Window Exhaustion / Slow Latency [Signed GCS URI] ──> ~100 Byte Reference ──> Zero Context Overhead / Low Latency

1. Preventing Hallucinations in Bounding Boxes & Style Specs

Multimodal models often struggle with numerical spatial coordinate grounding. When asked to generate normalized 2D bounding boxes ([ymin, xmin, ymax, xmax]), models may output inverted coordinates (x2​1000), or conflicting style specs.

Mitigation Strategies

  1. Strict Type-Coercion Schemas: Enforce Pydantic validation rules that catch invalid coordinate bounds before the payload reaches the generation API:
from pydantic import field_validator

class BoundingBox(BaseModel):
    box: list[int] = Field(description="Normalized coordinates [ymin, xmin, ymax, xmax] between 0 and 1000")

    @field_validator('box')
    def validate_coordinates(cls, v):
        if len(v) != 4:
            raise ValueError("Bounding box must contain exactly 4 coordinates.")
        ymin, xmin, ymax, xmax = v
        if ymax <= ymin or xmax <= xmin:
            raise ValueError("Invalid box dimensions: max coordinates must exceed min coordinates.")
        return v
Enter fullscreen mode Exit fullscreen mode
  1. Enum Enforcements: Constrain artistic parameters to closed lists (Literal["16:9", "1:1", "9:16"]) rather than free-form text strings to prevent invalid configuration errors.

2. Payload Transport: Base64 vs. Direct GCS Signed URLs

When an image generator produces a 4K image, returning the raw asset to the agent context presents an architectural decision: Base64 Inline Strings vs. Direct Storage Bucket URIs.

The Gold Standard Architecture

Never return Base64 image payloads back across the MCP tool execution boundary into the agent’s context window. Instead:

  1. The FastMCP tool streams generated binary outputs directly to a secure Google Cloud Storage (GCS) bucket.
  2. The tool generates an authorized GCS Signed URL or gs:// URI.
  3. The tool returns only the light string URI reference back to the LLM context window. Downstream clients parse the URI to display the media asset directly in the user interface.

4. Implementation: Production FastMCP Tool with GCS Bucket Offloading

Below is a complete, production-grade Python FastMCP server implementation that connects to an image generation pipeline, validates spatial/style arguments, streams output binaries directly to Google Cloud Storage, and returns a signed URI reference:

import os
import time
import uuid
import torch
import torch.nn as nn
from typing import Literal, List
from pydantic import BaseModel, Field, field_validator
from fastmcp import FastMCP
from google.cloud import storage

# Initialize FastMCP Server
mcp = FastMCP("Production-NanoBananaPro-Tool")

# --- 1. Payload Schema with Strict Grounding Guardrails ---
class BoundingBox(BaseModel):
    label: str
    coords: List[int] = Field(description="Normalized [ymin, xmin, ymax, xmax] scale 0-1000")

    @field_validator("coords")
    def validate_bounds(cls, v):
        if len(v) != 4 or not all(0 <= c <= 1000 for c in v):
            raise ValueError("Coordinates must be 4 integers scaled between 0 and 1000.")
        if v[2] <= v[0] or v[3] <= v[1]:
            raise ValueError("Max coordinates (ymax, xmax) must be greater than min coordinates (ymin, xmin).")
        return v

class ImageGenerationRequest(BaseModel):
    prompt: str = Field(description="High-fidelity visual prompt for Nano Banana Pro.")
    aspect_ratio: Literal["16:9", "1:1", "9:16", "21:9"] = Field(default="16:9")
    resolution: Literal["1K", "2K", "4K"] = Field(default="2K")
    bounding_boxes: List[BoundingBox] = Field(default=[], description="Optional object layout bounds.")

# --- 2. GCS Storage Offloading Helper ---
def upload_binary_to_gcs(bucket_name: str, binary_data: bytes, content_type: str = "image/png") -> str:
    """Uploads binary data directly to GCS and returns a Signed URL reference."""
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name)
    blob_name = f"generated_assets/{uuid.uuid4()}.png"
    blob = bucket.blob(blob_name)

    blob.upload_from_string(binary_data, content_type=content_type)

    # Generate temporary signed URL valid for 1 hour
    signed_url = blob.generate_signed_url(
        version="v4",
        expiration=3600,
        method="GET"
    )
    return signed_url

# --- 3. FastMCP Tool Execution Node ---
@mcp.tool()
async def generate_multimodal_asset(payload: ImageGenerationRequest) -> str:
    """
    Triggers Nano Banana Pro engine to render high-resolution images.
    Returns a secure GCS signed URL reference.
    """
    bucket_name = os.getenv("GCS_ASSET_BUCKET", "agent-multimodal-assets")

    # Simulate latent pipeline generation (outputs raw PNG byte array)
    print(f"[Tool Execution] Rendering prompt: '{payload.prompt}' at {payload.resolution} ({payload.aspect_ratio})...")

    # Mocking binary bytes (e.g., generated PNG output from API/Diffusion pipeline)
    mock_png_bytes = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"

    try:
        # Offload heavy binary payload to GCS rather than returning Base64 to LLM context
        signed_image_uri = upload_binary_to_gcs(
            bucket_name=bucket_name,
            binary_data=mock_png_bytes
        )
        return f"ASSET_GENERATED_SUCCESSFULLY: {signed_image_uri}"
    except Exception as e:
        # Graceful fallback error reporting
        return f"ERROR_FAILED_TO_STORE_ASSET: {str(e)}"

if __name__ == " __main__":
    # Server executes via stdio or HTTP SSE socket
    print("FastMCP Multimodal Asset Server initialized.")
    mcp.run()
Enter fullscreen mode Exit fullscreen mode

The integration of Gemini 3 Pro (Nano Banana Pro) into multi-agent systems via FastMCP wrappers transforms image generation from an unreliable external request into a deterministic tool primitive.

By enforcing strict JSON Schema guardrails at the tool boundary, offloading binary assets directly to cloud storage buckets like GCS, and returning lightweight signed URIs, developers can build scalable, multimodal AI agent runtimes without risking context window bloat or parameter hallucinations.

Need High-Impact Technical Content for Your Engineering Team?

I partner with developer-tooling startups, SaaS platforms, and engineering teams to translate complex infrastructure, agentic systems, and backend architecture into publication-grade technical writing.

Whether you need deep-dive architecture essays, hands-on developer tutorials, or technical counter-narratives:

Top comments (0)