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,
)
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'}}
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
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")
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")
@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())]
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
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
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
Name: google-genai
Version: 2.22.0
Name: mcp
Version: 2.2.0
kiro-cli 2.21.4
Lint and Test
make lint
ruff check .
All checks passed!
ruff format --check .
6 files already formatted
mypy .
Success: no issues found in 2 source files
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
----------------------------------------------------------------------
Ran 12 tests in 0.412s
OK
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
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
π’ 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
}
}
}
π 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
π€ default:
kiro_default
β’ aws-mcp uvx
β’ nb2lite bash
Scope : π€ default
Agent : kiro_default
Command : bash
Timeout : 120000 ms
Disabled: false
Env Vars: (none)
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
It starts the Kiro Agent Server, which logs its own version:
[INFO] kas.server.starting {"product":"KAS (Kiro Agent Server)","version":"0.63.3"}
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."
[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
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. ..."
[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`
--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
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."
[tool] @nb2lite/get_help
[tool] status: Completed
### π NB2Lite Agent (gemini-3.1-flash-lite-image) Help & Configuration
β
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."
| 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
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
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
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
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
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")
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
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
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 toserver.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.keyat launch - β οΈ Kiro 3 denies MCP calls in
--no-interactivemode, even withtrustedToolsset in the workspacecli.json - π’
--trust-all-toolsor apermissions.yamlrule fornb2lite/*both unblock it, andverify-livepassed 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
- nb2lite-kiro | GitHub
- nb2lite | GitHub
- Nano Banana 2 Lite, Revisited: MCP 2.0, the New Interactions API, and Three Agent CLIs
- Interactions API breaking changes | Gemini API
- Interactions API | Gemini API
- Migration Guide: v1 to v2 | MCP Python SDK
- FastMCP Is Now MCPServer: Migrating a Python MCP Server to the MCP SDK 2.x
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)