DEV Community

Cover image for Nano Banana 2 Lite in Kiro CLI 3: MCP 2.0, the New Interactions API, and Headless Permissions
xbill for Google Developer Experts

Posted on

Nano Banana 2 Lite in Kiro CLI 3: MCP 2.0, the New Interactions API, and Headless Permissions

This article provides a step by step update guide for a Python MCP server that drives Google Nano Banana 2 Lite (gemini-3.1-flash-lite-image) through the Gemini Interactions API, running inside Kiro CLI 3. Two dependency lines moved underneath the server: the MCP Python SDK went to 2.x, and the Interactions API dropped the schema that google-genai 1.x speaks. The server is then registered with Kiro, given a permission rule, and validated end to end against the live API from a headless Kiro 3 session.

https://github.com/xbill9/nb2lite-kiro


Haven't You Done This One Before?

What is old is new β€” again.

The same update was written up for Claude Code, Codex and Antigravity CLI:

Nano Banana 2 Lite, Revisited: MCP 2.0, the New Interactions API, and Three Agent CLIs

This is the Kiro edition. nb2lite-kiro tracks xbill9/nb2lite, and server.py, test_agent.py, requirements.txt and the Makefile are byte-identical between the two. Everything that differs is how Kiro launches the server, how it is allowed to call it, and where it finds the skill.

Before After
MCP SDK mcp.server.fastmcp.FastMCP mcp.server.mcpserver.MCPServer
google-genai unpinned, 1.x google-genai>=2,<3
Tools 4 5 β€” adds edit_local_image_with_style
Kiro server name nb2lite-agent nb2lite
API key written into mcp.json and .env read from ~/gemini.key at launch
Live check manual .kiro/skills/verify-live

What is Nano Banana 2 Lite?

Nano Banana 2 Lite is the nickname for Gemini 3.1 Flash-Lite Image, Google's low-latency image generation and editing model:

Gemini 3.1 Flash-Lite Image (Nano Banana 2 Lite) | Google Cloud Documentation


So What is the Secret Sauce?

The Interactions API. Every call is stored server-side with store=True and returns an interaction ID. Pass that ID back as previous_interaction_id and the model edits the image it already made, instead of redrawing a scene from a fresh prompt.

That keeps the MCP surface small. generate_image starts a session, edit_image continues one, and Kiro only has to carry an ID between turns.


What Broke in the Interactions API?

The server's code did not change. The API moved away from the SDK it was installed with.

This is the call server.py makes, run with google-genai 1.x from a scratch install:

c = genai.Client(api_key=...)
c.interactions.create(
    model="gemini-3.1-flash-lite-image",
    input="a small red cube on a white table",
    response_format={"type": "image"},
    generation_config={"thinking_level": "minimal"},
    store=True,
)
Enter fullscreen mode Exit fullscreen mode
google-genai 1.75.0
BadRequestError: Error code: 400 - {'error': {'message': 'The legacy Interactions API schema is no longer supported. Please upgrade your google-genai Python SDK to version >= 2.0.0 (e.g., run pip install -U google-genai) to use the Interactions API. For details and migration examples, see: https://ai.google.dev/gemini-api/docs/interactions-breaking-changes-may-2026', 'code': 'invalid_request'}}
Enter fullscreen mode Exit fullscreen mode

The message names the fix. Inside Kiro it is easy to miss: every tool catches the exception and returns it as a πŸ”΄ string, so the agent reports "Image generation failed" with the version number buried in the text.


What Changed in the Response?

google-genai 2.x reads the new schema, where the model's output arrives as a list of steps. The SDK exposes the generated image as interaction.output_image, with data and mime_type.

server.py already read output_image, so upgrading the SDK was the whole fix:

image_output = getattr(interaction, "output_image", None)
...
data = getattr(image_output, "data", None)
if isinstance(data, str):
    image_bytes = base64.b64decode(data)
else:
    image_bytes = data
Enter fullscreen mode Exit fullscreen mode

The legacy schema was removed on 2026-06-08.


πŸ”Ž Tip: Mocked Tests Cannot See This Break

The unit tests mock _get_client, so the SDK never builds a real response and they pass against a broken API. One test now builds a real steps-schema Interaction with the SDK's own model and runs it through the response handler:

interaction = Interaction.model_validate(
    {
        "id": "int_steps",
        "status": "completed",
        "steps": [
            {
                "type": "model_output",
                "content": [
                    {"type": "image", "data": "aGVsbG8=", "mime_type": "image/png"}
                ],
            }
        ],
    }
)
result = _handle_response(interaction, "steps")
Enter fullscreen mode Exit fullscreen mode

On google-genai 1.x that import does not exist, so the test fails loudly instead of the API failing quietly. The rest of the gap is the live check later in this article.


What Changed for MCP 2.0?

The import and the constructor:

-from mcp.server.fastmcp import FastMCP
+from mcp.server.mcpserver import MCPServer

-# Initialize FastMCP Server
-mcp = FastMCP("NB2Lite Agent")
+# Initialize MCP Server (mcp>=2 renamed FastMCP to MCPServer)
+mcp = MCPServer("NB2Lite Agent")
Enter fullscreen mode Exit fullscreen mode

@mcp.tool(), mcp.run() and every tool body stay as they are. The full walk-through of the 2.x changes is in the companion article:

FastMCP Is Now MCPServer: Migrating a Python MCP Server to the MCP SDK 2.x

list_tools() is async on MCPServer. The old test reached into a private attribute:

-        tools = [t.name for t in mcp._tool_manager.list_tools()]
+        tools = [t.name for t in asyncio.run(mcp.list_tools())]
Enter fullscreen mode Exit fullscreen mode

Both requirements now carry a floor and a ceiling, so the next major version arrives on purpose:

-google-genai
-mcp
+google-genai>=2,<3
+mcp>=2,<3
Enter fullscreen mode Exit fullscreen mode

At This Point You Should Have…

  • Python 3.10 or newer, installed globally β€” no virtualenv
  • A Gemini API key from Google AI Studio
  • Kiro CLI installed and logged in

Setup the Basic Environment

cd ~
git clone https://github.com/xbill9/nb2lite-kiro
cd nb2lite-kiro
make install
source set_env.sh
Enter fullscreen mode Exit fullscreen mode

set_env.sh reads the key from ~/gemini.key, or prompts for it and saves it there with mode 600. It then rewrites the nb2lite entry in .kiro/settings/mcp.json with this checkout's path.

python3 -m pip show mcp google-genai | grep -E "^(Name|Version)"
kiro-cli --version
Enter fullscreen mode Exit fullscreen mode
Name: google-genai
Version: 2.22.0
Name: mcp
Version: 2.2.0
kiro-cli 2.21.4
Enter fullscreen mode Exit fullscreen mode

Lint and Test

make lint
Enter fullscreen mode Exit fullscreen mode
ruff check .
All checks passed!
ruff format --check .
6 files already formatted
mypy .
Success: no issues found in 2 source files
Enter fullscreen mode Exit fullscreen mode

mypy is not in requirements.txt. On this machine make lint first failed with make: mypy: No such file or directory, and python3 -m pip install mypy fixed it.

make test
Enter fullscreen mode Exit fullscreen mode
----------------------------------------------------------------------
Ran 12 tests in 0.412s

OK
Enter fullscreen mode Exit fullscreen mode

Test the Protocol by Hand

Kiro speaks JSON-RPC over stdio, so test that too. Hold stdin open with sleep, or the server sees end-of-input before it answers:

{ printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; sleep 4; } \
  | python3 server.py 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

Summarised:

initialize OK: name='NB2Lite Agent' version='' proto 2025-06-18
tools/list OK: 5 tools -> generate_image, edit_image, edit_local_image, edit_local_image_with_style, get_help
Enter fullscreen mode Exit fullscreen mode

🟒 Five tools. The blank version is what an unversioned MCP 2.x server reports.


Registering the Server with Kiro

The workspace config registers the server as nb2lite:

{
  "mcpServers": {
    "nb2lite": {
      "command": "bash",
      "args": [
        "-c",
        "GEMINI_API_KEY=$(cat ~/gemini.key) exec python3 /home/xbill/nb2lite-kiro/server.py"
      ],
      "disabled": false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ž Tip: the old setup put the key in the config. The previous init.sh injected GEMINI_API_KEY into the server's env block in mcp.json and wrote a .env file beside it. The bash -c launch reads ~/gemini.key each time Kiro starts the server, so the key never lands in a file inside the repository.

kiro-cli mcp list
kiro-cli mcp status --name nb2lite
Enter fullscreen mode Exit fullscreen mode
πŸ€– default:
  kiro_default
    β€’ aws-mcp      uvx
    β€’ nb2lite      bash

Scope   : πŸ€– default
Agent   : kiro_default
Command : bash
Timeout : 120000 ms
Disabled: false
Env Vars: (none)
Enter fullscreen mode Exit fullscreen mode

Env Vars: (none) is the point: nothing secret in the registration.


Kiro CLI 3 is Opt-In

On kiro-cli 2.21.4, the next generation agent is a flag:

kiro-cli chat --v3
Enter fullscreen mode Exit fullscreen mode

It starts the Kiro Agent Server, which logs its own version:

[INFO] kas.server.starting {"product":"KAS (Kiro Agent Server)","version":"0.63.3"}
Enter fullscreen mode Exit fullscreen mode

The mcp.json above works unchanged under v3, and skills in .kiro/skills/<name>/SKILL.md are picked up the same way.


Validation with Kiro CLI 3: Headless is Denied

Interactive Kiro asks before each MCP tool call. A non-interactive session has nobody to ask:

kiro-cli chat --v3 --no-interactive "Call the nb2lite MCP server's get_help tool. Reply with only the first line of its output verbatim, then the names of the nb2lite tools available to you."
Enter fullscreen mode Exit fullscreen mode
[tool] @nb2lite/get_help
[denied] tool permission approval is not supported in non-interactive mode. Use --trust-all-tools to auto-approve.
[tool] status: Failed
Enter fullscreen mode Exit fullscreen mode

The workspace .kiro/settings/cli.json in this repository sets "trustedTools": ["*"]. The v3 session denied the call anyway.

The error names one fix, and it works:

kiro-cli chat --v3 --no-interactive --trust-all-tools "Call the nb2lite MCP server's get_help tool. ..."
Enter fullscreen mode Exit fullscreen mode
[tool] @nb2lite/get_help
[tool] status: Completed
First line: `### 🌌 NB2Lite Agent (gemini-3.1-flash-lite-image) Help & Configuration`

Available nb2lite tools:
- `generate_image`
- `edit_image`
- `edit_local_image`
- `edit_local_image_with_style`
- `get_help`
Enter fullscreen mode Exit fullscreen mode

--trust-all-tools trusts every tool, including shell. The narrower fix is a rule.


Allow Just the Image Tools with permissions.yaml

Kiro 3 reads permission rules from permissions.yaml: globally in ~/.kiro/settings/, or per workspace under ~/.kiro/workspace-roots/<hash>/. Both live outside the repository, so a checkout cannot grant itself permissions.

rules:
  - capability: mcp
    match: ["nb2lite/*"]
    effect: allow
Enter fullscreen mode Exit fullscreen mode

With that file in ~/.kiro/settings/ and no trust flag:

kiro-cli chat --v3 --no-interactive "Call the nb2lite MCP server's get_help tool. Reply with only the first line of its output verbatim."
Enter fullscreen mode Exit fullscreen mode
[tool] @nb2lite/get_help
[tool] status: Completed
### 🌌 NB2Lite Agent (gemini-3.1-flash-lite-image) Help & Configuration
Enter fullscreen mode Exit fullscreen mode

βœ… MCP tools are addressed as <server>/<tool>. A rule accepts only capability, effect, match and exclude; the parser in the shipped agent server rejects any other key as an unknown field.


There is A Skill for That!

Mocked tests pass while the API is broken, so the repository ships .kiro/skills/verify-live/SKILL.md. It runs the unit tests and lint, then chains all four image tools through the running MCP server at thinking_level="minimal", and opens every image it saved.

Run headless from Kiro 3:

kiro-cli chat --v3 --no-interactive --trust-all-tools "Run the verify-live skill exactly as written. For every nb2lite tool call, print the call and the tool's full result text verbatim, then your inspection of the image. Finish with a pass/fail table per step."
Enter fullscreen mode Exit fullscreen mode
Step Tool Result
1 generate_image β€” a red cube on a white table 🟒 red cube, white table
2 edit_image β€” make the cube blue 🟒 same composition, only the cube recoloured
3 edit_local_image β€” add a green sphere 🟒 sphere added beside the cube
4 generate_image β€” watercolor sunflowers 🟒 the style reference
5 edit_local_image_with_style β€” cube in the reference's style 🟒 the cube scene as a watercolor, no sunflowers

Kiro followed the skill's ordering, calling steps 1 and 4 together and then 2, 3 and 5 together, and deleted the images once it had inspected them. Step 2 is the Interactions API test, since it proves the stored session came back.


Enough, Already! Show me the Money!

The same headless Kiro 3 session style, with four calls chained by the agent:

generate_image(prompt="a friendly pixel-art ghost banana with big eyes typing on a tiny glowing laptop, dark indigo background, crisp 16-bit style", thinking_level="minimal", aspect_ratio="16:9")

🟒 Image successfully saved!
β€’ Saved to: /home/xbill/nb2lite-kiro/gen_1789484185_cb29a061.jpg
β€’ Interaction ID: v1_ChdtRnlwYXBPRUtfbXUxTWtQNFB1UjRBVRIXbUZ5cGFwT0VLX211MU1rUDRQdVI0QVU
Enter fullscreen mode Exit fullscreen mode

A glowing pixel-art ghost banana typing on a small laptop in a dark indigo room with a moonlit window and shelves

Continue the stored session with the interaction ID:

edit_image(previous_interaction_id="v1_ChdtRnlwYXBPRUtfbXUxTWtQNFB1UjRBVRIXbUZ5cGFwT0VLX211MU1rUDRQdVI0QVU", edit_prompt="give the banana a small wizard hat and make the laptop screen show green terminal text", thinking_level="minimal")

🟒 Image successfully saved!
β€’ Saved to: /home/xbill/nb2lite-kiro/edit_1789484197_dd06d5b0.jpg
β€’ Interaction ID: v1_ChdtRnlwYXBPRUtfbXUxTWtQNFB1UjRBVRIXcEZ5cGFxNmREOUNhOU1vUHR1REpvQUk
Enter fullscreen mode Exit fullscreen mode

The same ghost banana in the same room and pose, now wearing a starry wizard hat, with green terminal text on the laptop

The window, the shelves, the lantern and the pose all carried over. Only the hat and the screen changed.

The new tool takes a style from a second image. First, a reference:

generate_image(prompt="a Bauhaus poster, flat geometric shapes, primary colors, heavy black lines, off-white paper", thinking_level="minimal", aspect_ratio="16:9")

🟒 Image successfully saved!
β€’ Saved to: /home/xbill/nb2lite-kiro/gen_1789484204_75f1e1e0.jpg
Enter fullscreen mode Exit fullscreen mode

A generated Bauhaus-style poster with a black circle, diagonal black bars, a red square and yellow triangles on off-white paper

The model added poster lettering on its own β€” an exhibition, a venue and dates. All of it is invented.

Then the original banana, in that style:

edit_local_image_with_style(image_path="/home/xbill/nb2lite-kiro/gen_1789484185_cb29a061.jpg", style_image_path="/home/xbill/nb2lite-kiro/gen_1789484204_75f1e1e0.jpg", edit_prompt="keep the banana, its eyes and the laptop", thinking_level="minimal", aspect_ratio="16:9")

🟒 Image successfully saved!
β€’ Saved to: /home/xbill/nb2lite-kiro/style_edit_1789484214_51da1f65.jpg
Enter fullscreen mode Exit fullscreen mode

The ghost banana at its laptop redrawn as a flat illustration with black outlines on cream paper, over the poster's black circle and diagonals

The banana, the laptop and the room props came through. The poster's shapes and paper did too; its lettering did not.


πŸ”Ž About That Cover

The cover of this article was generated by the server this article describes, headless from Kiro 3. One generate_image call at thinking_level="high", because the cover carries lettering:

generate_image(prompt="A wide tech blog cover illustration, all important content kept inside a central horizontal band ... Large crisp title text in the center band: 'Nano Banana 2 Lite in Kiro CLI 3'. Smaller subtitle beneath it: 'MCP 2.0 + the new Interactions API'. Accurate, typo-free lettering.", aspect_ratio="16:9", thinking_level="high")

🟒 Image successfully saved!
β€’ Saved to: /home/xbill/nb2lite-kiro/gen_1789484250_dd316079.jpg
β€’ Interaction ID: v1_ChcyVnlwYXBpUEVhalZqckVQdTRLeGlBTRIXMlZ5cGFwaVBFYWpWanJFUHU0S3hpQU0
Enter fullscreen mode Exit fullscreen mode

The subtitle came out exact. The title lost a word β€” "Nano Banana 2 Lite Kiro CLI 3" β€” and a line on the tiny terminal screen read Kire CLI. That is the stateful edit loop's job, so the fix was an edit_image on the same interaction:

edit_image(previous_interaction_id="v1_ChcyVnlwYXBpUEVhalZqckVQdTRLeGlBTRIXMlZ5cGFwaVBFYWpWanJFUHU0S3hpQU0", edit_prompt="Fix only the lettering. The title must read exactly 'Nano Banana 2 Lite in Kiro CLI 3', and any on-screen text reading 'Kire' must read 'Kiro'. Keep everything else exactly the same.", thinking_level="high")
Enter fullscreen mode Exit fullscreen mode

That put "in" back and dropped "Lite". A second edit, naming each line of the title separately, got the title exact:

edit_image(previous_interaction_id="v1_ChcyVnlwYXBpUEVhalZqckVQdTRLeGlBTRIXQ0YycGF1bUxNdGFZak1jUHlaQ3prUW8", edit_prompt="Fix only two pieces of lettering. The two-line title must read 'Nano Banana 2 Lite' on the first line and 'in Kiro CLI 3' on the second line. On the computer screen, the bottom line 'Kire CLI' must read 'Kiro CLI'. Keep everything else exactly the same.", thinking_level="high")

🟒 Image successfully saved!
β€’ Saved to: /home/xbill/nb2lite-kiro/edit_1789484421_431dff69.jpg
Enter fullscreen mode Exit fullscreen mode

The characters, frames and composition held through both edits. The screen line did not: Kire CLI is still there, in the bottom line of the terminal. On this cover, the headline took two edits and the few-pixel screen text never came right. No retouching, beyond cropping the 16:9 output to the box dev.to displays.


Cheat Sheet

# pins
#   google-genai>=2,<3   (1.x: 400 "legacy Interactions API schema is no longer supported")
#   mcp>=2,<3            (FastMCP -> MCPServer)
make install && make lint && make test     # lint needs mypy installed
source set_env.sh                          # key from ~/gemini.key, path into .kiro/settings/mcp.json

# check the registration
kiro-cli mcp list && kiro-cli mcp status --name nb2lite

# Kiro 3
kiro-cli chat --v3

# headless: either trust everything...
kiro-cli chat --v3 --no-interactive --trust-all-tools "Run the verify-live skill"

# ...or allow only nb2lite, in ~/.kiro/settings/permissions.yaml
# rules:
#   - capability: mcp
#     match: ["nb2lite/*"]
#     effect: allow
Enter fullscreen mode Exit fullscreen mode

Summary

The goal of this article was to bring the Kiro edition of the Nano Banana 2 Lite MCP server back to a working state on current dependencies, and to run it from Kiro CLI 3. The key to the solution was reading the error messages, which named every fix, and then proving the live API path from a headless Kiro session instead of trusting mocked tests. The update results were:

  • ❌ google-genai 1.x gets HTTP 400 from the Interactions API; the fix was google-genai>=2,<3, with no change to server.py
  • 🟒 The MCP 2.0 port was the import and the constructor; all five tools register unchanged
  • 🟒 The API key moved out of mcp.json; the server reads ~/gemini.key at launch
  • ⚠️ Kiro 3 denies MCP calls in --no-interactive mode, even with trustedTools set in the workspace cli.json
  • 🟒 --trust-all-tools or a permissions.yaml rule for nb2lite/* both unblock it, and verify-live passed all five steps

Scope: one Debian 13 workstation, Python 3.14.7, mcp 2.2.0 and google-genai 2.22.0, with google-genai 1.75.0 as the failing reference from the upstream repository's run on the identical server.py. kiro-cli 2.21.4 with --v3 (KAS 0.63.3), every Kiro run headless with --no-interactive; the interactive approval prompt was not exercised. Every demo image call ran once at thinking_level="minimal" against gemini-3.1-flash-lite-image; nothing here measures latency or cost.

The strategy for using MCP with Nano Banana 2 Lite in Kiro CLI 3 was validated with an incremental step by step approach.

References


mcp 2.2.0, google-genai 2.22.0, Python 3.14.7, ruff 0.16.7, mypy 2.3.1, kiro-cli 2.21.4 (KAS 0.63.3), gemini-3.1-flash-lite-image.

Top comments (0)