google-flow-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google_flow_mcp/__init__.py +3 -0
- google_flow_mcp/server.py +551 -0
- google_flow_mcp-0.1.0.dist-info/METADATA +134 -0
- google_flow_mcp-0.1.0.dist-info/RECORD +7 -0
- google_flow_mcp-0.1.0.dist-info/WHEEL +4 -0
- google_flow_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- google_flow_mcp-0.1.0.dist-info/licenses/LICENSE +139 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""Google Flow MCP server — 6 tools for Google AI image and video generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import io
|
|
7
|
+
import mimetypes
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from google import genai
|
|
15
|
+
from google.genai import types
|
|
16
|
+
from mcp.server.fastmcp import FastMCP
|
|
17
|
+
from PIL import Image as PILImage
|
|
18
|
+
|
|
19
|
+
# ── Constants ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
MODEL_IMAGE_PRO = "gemini-3-pro-image" # Nano Banana Pro
|
|
22
|
+
MODEL_IMAGE_FLASH = "gemini-3.1-flash-image" # Nano Banana 2
|
|
23
|
+
MODEL_VIDEO = "veo-3.1-generate-preview" # Veo 3.1
|
|
24
|
+
|
|
25
|
+
VIDEO_POLL_INTERVAL = 10 # seconds between Veo status polls
|
|
26
|
+
VIDEO_POLL_TIMEOUT = 600 # 10-minute max wait for video generation
|
|
27
|
+
|
|
28
|
+
# ── FastMCP Instance ─────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
mcp = FastMCP(
|
|
31
|
+
name="google-flow-mcp",
|
|
32
|
+
instructions=(
|
|
33
|
+
"Provides 6 tools for Google AI image and video generation. "
|
|
34
|
+
"Image tools use Nano Banana Pro (gemini-3-pro-image, high quality) or "
|
|
35
|
+
"Nano Banana 2 (gemini-3.1-flash-image, fast). "
|
|
36
|
+
"Video tools use Veo 3.1 (veo-3.1-generate-preview) with native audio. "
|
|
37
|
+
"Image generation works on the free tier. "
|
|
38
|
+
"Video generation requires a paid Google AI API plan. "
|
|
39
|
+
"Outputs are saved to ~/google_flow_outputs/ by default (override with FLOW_OUTPUT_DIR)."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ── Client ────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
_client: genai.Client | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_client() -> genai.Client:
|
|
49
|
+
api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
|
50
|
+
if not api_key:
|
|
51
|
+
raise EnvironmentError(
|
|
52
|
+
"GOOGLE_API_KEY environment variable is required. "
|
|
53
|
+
"Get a free key at https://aistudio.google.com/apikey"
|
|
54
|
+
)
|
|
55
|
+
return genai.Client(api_key=api_key)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_client() -> genai.Client:
|
|
59
|
+
global _client
|
|
60
|
+
if _client is None:
|
|
61
|
+
_client = _get_client()
|
|
62
|
+
return _client
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── File Helpers ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _output_dir() -> Path:
|
|
69
|
+
base = os.environ.get("FLOW_OUTPUT_DIR") or str(Path.home() / "google_flow_outputs")
|
|
70
|
+
path = Path(base)
|
|
71
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _timestamped(prefix: str, ext: str, out: Optional[Path] = None) -> Path:
|
|
76
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
77
|
+
return (out or _output_dir()) / f"{prefix}_{ts}.{ext}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _save_image(image_obj, path: Path) -> None:
|
|
81
|
+
"""Save a generated image to disk, handling multiple SDK response shapes."""
|
|
82
|
+
if hasattr(image_obj, "save"):
|
|
83
|
+
image_obj.save(str(path))
|
|
84
|
+
elif getattr(image_obj, "image_bytes", None):
|
|
85
|
+
PILImage.open(io.BytesIO(image_obj.image_bytes)).save(str(path))
|
|
86
|
+
elif getattr(image_obj, "_image_bytes", None):
|
|
87
|
+
PILImage.open(io.BytesIO(image_obj._image_bytes)).save(str(path))
|
|
88
|
+
else:
|
|
89
|
+
raise RuntimeError(f"Cannot save image of type {type(image_obj).__name__}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _load_image_bytes(path: str) -> bytes:
|
|
93
|
+
with open(path, "rb") as f:
|
|
94
|
+
return f.read()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _mime(path: str) -> str:
|
|
98
|
+
mime, _ = mimetypes.guess_type(path)
|
|
99
|
+
return mime or "image/png"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_image_model(model: str) -> str:
|
|
103
|
+
return MODEL_IMAGE_FLASH if model.lower() in ("flash", "2") else MODEL_IMAGE_PRO
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _poll_operation(client: genai.Client, operation) -> object:
|
|
107
|
+
"""Poll a Veo long-running operation until done or timeout."""
|
|
108
|
+
deadline = time.monotonic() + VIDEO_POLL_TIMEOUT
|
|
109
|
+
while not operation.done:
|
|
110
|
+
if time.monotonic() > deadline:
|
|
111
|
+
raise TimeoutError(
|
|
112
|
+
f"Video generation timed out after {VIDEO_POLL_TIMEOUT // 60} minutes. "
|
|
113
|
+
"Try a shorter duration or check the Google AI console."
|
|
114
|
+
)
|
|
115
|
+
await asyncio.sleep(VIDEO_POLL_INTERVAL)
|
|
116
|
+
operation = await client.aio.operations.get(operation)
|
|
117
|
+
if getattr(operation, "error", None) and operation.error.message:
|
|
118
|
+
raise RuntimeError(f"Video generation failed: {operation.error.message}")
|
|
119
|
+
return operation
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _collect_video_results(operation, out: Path, ts: str) -> tuple[list[str], list[str]]:
|
|
123
|
+
"""Extract saved paths and/or URIs from a completed video operation."""
|
|
124
|
+
saved, uris = [], []
|
|
125
|
+
videos = getattr(operation.response, "generated_videos", [])
|
|
126
|
+
for i, gen_video in enumerate(videos):
|
|
127
|
+
vid = gen_video.video
|
|
128
|
+
vid_bytes = getattr(vid, "video_bytes", None)
|
|
129
|
+
uri = getattr(vid, "uri", None)
|
|
130
|
+
if vid_bytes:
|
|
131
|
+
path = out / f"flow_video_{ts}_{i}.mp4"
|
|
132
|
+
path.write_bytes(vid_bytes)
|
|
133
|
+
saved.append(str(path))
|
|
134
|
+
elif uri:
|
|
135
|
+
uris.append(uri)
|
|
136
|
+
return saved, uris
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _format_video_result(saved: list[str], uris: list[str]) -> str:
|
|
140
|
+
parts = []
|
|
141
|
+
if saved:
|
|
142
|
+
parts.append("Saved videos:\n" + "\n".join(saved))
|
|
143
|
+
if uris:
|
|
144
|
+
parts.append(
|
|
145
|
+
"Video URI(s) (available for 2 days on Google servers):\n" + "\n".join(uris)
|
|
146
|
+
)
|
|
147
|
+
parts.append("Note: All Veo videos are SynthID-watermarked by Google.")
|
|
148
|
+
return "\n\n".join(parts)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── Tool 1: Text → Image ─────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
async def flow_generate_image(
|
|
156
|
+
prompt: str,
|
|
157
|
+
model: str = "pro",
|
|
158
|
+
number_of_images: int = 1,
|
|
159
|
+
aspect_ratio: str = "1:1",
|
|
160
|
+
negative_prompt: Optional[str] = None,
|
|
161
|
+
output_dir: Optional[str] = None,
|
|
162
|
+
) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Generate 1–4 images from a text prompt using Google AI.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
prompt: Detailed description of the image(s) to create.
|
|
168
|
+
model: 'pro' for Nano Banana Pro (high quality) or 'flash' for Nano Banana 2 (fast).
|
|
169
|
+
number_of_images: How many images to generate (1–4).
|
|
170
|
+
aspect_ratio: '1:1', '3:4', '4:3', '9:16', or '16:9'.
|
|
171
|
+
negative_prompt: What to exclude from the generated images.
|
|
172
|
+
output_dir: Override the output directory (default: ~/google_flow_outputs).
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Paths to the saved image files.
|
|
176
|
+
"""
|
|
177
|
+
client = get_client()
|
|
178
|
+
model_id = _resolve_image_model(model)
|
|
179
|
+
n = max(1, min(4, number_of_images))
|
|
180
|
+
out = Path(output_dir) if output_dir else _output_dir()
|
|
181
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
|
|
183
|
+
response = await client.aio.models.generate_images(
|
|
184
|
+
model=model_id,
|
|
185
|
+
prompt=prompt,
|
|
186
|
+
config=types.GenerateImagesConfig(
|
|
187
|
+
number_of_images=n,
|
|
188
|
+
aspect_ratio=aspect_ratio,
|
|
189
|
+
negative_prompt=negative_prompt,
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
194
|
+
paths = []
|
|
195
|
+
for i, gen_img in enumerate(response.generated_images):
|
|
196
|
+
path = out / f"flow_image_{ts}_{i}.png"
|
|
197
|
+
_save_image(gen_img.image, path)
|
|
198
|
+
paths.append(str(path))
|
|
199
|
+
|
|
200
|
+
return f"Generated {len(paths)} image(s):\n" + "\n".join(paths)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ── Tool 2: Edit Image ────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
async def flow_edit_image(
|
|
208
|
+
image_path: str,
|
|
209
|
+
prompt: str,
|
|
210
|
+
edit_mode: str = "inpaint_insertion",
|
|
211
|
+
model: str = "pro",
|
|
212
|
+
mask_mode: str = "MASK_MODE_BACKGROUND",
|
|
213
|
+
output_dir: Optional[str] = None,
|
|
214
|
+
) -> str:
|
|
215
|
+
"""
|
|
216
|
+
Edit an existing image using a natural language instruction.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
image_path: Absolute path to the source image file.
|
|
220
|
+
prompt: Natural language description of the edit to apply.
|
|
221
|
+
edit_mode: 'inpaint_insertion' (add), 'inpaint_removal' (remove),
|
|
222
|
+
'outpaint' (expand canvas), 'bgswap' (replace background).
|
|
223
|
+
model: 'pro' for Nano Banana Pro or 'flash' for Nano Banana 2.
|
|
224
|
+
mask_mode: Auto-mask strategy — 'MASK_MODE_BACKGROUND', 'MASK_MODE_FOREGROUND',
|
|
225
|
+
or 'MASK_MODE_SEMANTIC'.
|
|
226
|
+
output_dir: Override the output directory (default: ~/google_flow_outputs).
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Path to the edited image file.
|
|
230
|
+
"""
|
|
231
|
+
client = get_client()
|
|
232
|
+
model_id = _resolve_image_model(model)
|
|
233
|
+
out = Path(output_dir) if output_dir else _output_dir()
|
|
234
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
|
|
236
|
+
_edit_mode_map = {
|
|
237
|
+
"inpaint_insertion": "EDIT_MODE_INPAINT_INSERTION",
|
|
238
|
+
"inpaint_removal": "EDIT_MODE_INPAINT_REMOVAL",
|
|
239
|
+
"outpaint": "EDIT_MODE_OUTPAINT",
|
|
240
|
+
"bgswap": "EDIT_MODE_BGSWAP",
|
|
241
|
+
}
|
|
242
|
+
sdk_edit_mode = _edit_mode_map.get(edit_mode, edit_mode)
|
|
243
|
+
|
|
244
|
+
raw_ref = types.RawReferenceImage(
|
|
245
|
+
reference_id=0,
|
|
246
|
+
reference_image=types.Image(
|
|
247
|
+
image_bytes=_load_image_bytes(image_path),
|
|
248
|
+
mime_type=_mime(image_path),
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
mask_ref = types.MaskReferenceImage(
|
|
252
|
+
reference_id=1,
|
|
253
|
+
config=types.MaskReferenceConfig(mask_mode=mask_mode),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
response = await client.aio.models.edit_image(
|
|
257
|
+
model=model_id,
|
|
258
|
+
prompt=prompt,
|
|
259
|
+
reference_images=[raw_ref, mask_ref],
|
|
260
|
+
config=types.EditImageConfig(
|
|
261
|
+
edit_mode=sdk_edit_mode,
|
|
262
|
+
number_of_images=1,
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
267
|
+
path = out / f"flow_edit_{ts}.png"
|
|
268
|
+
_save_image(response.generated_images[0].image, path)
|
|
269
|
+
return f"Edited image saved to:\n{path}"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── Tool 3: Generate with Reference Images ────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@mcp.tool()
|
|
276
|
+
async def flow_generate_image_with_references(
|
|
277
|
+
prompt: str,
|
|
278
|
+
reference_image_paths: list[str],
|
|
279
|
+
model: str = "pro",
|
|
280
|
+
aspect_ratio: str = "1:1",
|
|
281
|
+
number_of_images: int = 1,
|
|
282
|
+
output_dir: Optional[str] = None,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Generate an image guided by up to 14 reference images.
|
|
286
|
+
|
|
287
|
+
The model uses the reference images for style, composition, and subject guidance
|
|
288
|
+
while following the text prompt.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
prompt: Description of the image to generate.
|
|
292
|
+
reference_image_paths: List of paths to reference image files (max 14).
|
|
293
|
+
model: 'pro' for Nano Banana Pro or 'flash' for Nano Banana 2.
|
|
294
|
+
aspect_ratio: '1:1', '3:4', '4:3', '9:16', or '16:9'.
|
|
295
|
+
number_of_images: How many images to generate (1–4).
|
|
296
|
+
output_dir: Override the output directory (default: ~/google_flow_outputs).
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Paths to the generated image files.
|
|
300
|
+
"""
|
|
301
|
+
if len(reference_image_paths) > 14:
|
|
302
|
+
raise ValueError(
|
|
303
|
+
f"Maximum 14 reference images allowed; got {len(reference_image_paths)}."
|
|
304
|
+
)
|
|
305
|
+
if not reference_image_paths:
|
|
306
|
+
raise ValueError("At least one reference image path is required.")
|
|
307
|
+
|
|
308
|
+
client = get_client()
|
|
309
|
+
model_id = _resolve_image_model(model)
|
|
310
|
+
n = max(1, min(4, number_of_images))
|
|
311
|
+
out = Path(output_dir) if output_dir else _output_dir()
|
|
312
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
|
|
314
|
+
reference_images = [
|
|
315
|
+
types.RawReferenceImage(
|
|
316
|
+
reference_id=i,
|
|
317
|
+
reference_image=types.Image(
|
|
318
|
+
image_bytes=_load_image_bytes(ref_path),
|
|
319
|
+
mime_type=_mime(ref_path),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
for i, ref_path in enumerate(reference_image_paths)
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
response = await client.aio.models.generate_images(
|
|
326
|
+
model=model_id,
|
|
327
|
+
prompt=prompt,
|
|
328
|
+
reference_images=reference_images,
|
|
329
|
+
config=types.GenerateImagesConfig(
|
|
330
|
+
number_of_images=n,
|
|
331
|
+
aspect_ratio=aspect_ratio,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
336
|
+
paths = []
|
|
337
|
+
for i, gen_img in enumerate(response.generated_images):
|
|
338
|
+
path = out / f"flow_ref_image_{ts}_{i}.png"
|
|
339
|
+
_save_image(gen_img.image, path)
|
|
340
|
+
paths.append(str(path))
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
f"Generated {len(paths)} image(s) using {len(reference_image_paths)} reference(s):\n"
|
|
344
|
+
+ "\n".join(paths)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ── Tool 4: Text → Video ─────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@mcp.tool()
|
|
352
|
+
async def flow_generate_video(
|
|
353
|
+
prompt: str,
|
|
354
|
+
duration_seconds: int = 8,
|
|
355
|
+
aspect_ratio: str = "16:9",
|
|
356
|
+
negative_prompt: Optional[str] = None,
|
|
357
|
+
anchor_frame_path: Optional[str] = None,
|
|
358
|
+
enhance_prompt: bool = True,
|
|
359
|
+
output_dir: Optional[str] = None,
|
|
360
|
+
) -> str:
|
|
361
|
+
"""
|
|
362
|
+
Generate a cinematic video with native audio from a text prompt using Veo 3.1.
|
|
363
|
+
|
|
364
|
+
Requires a paid Google AI API plan. Generated videos are SynthID-watermarked
|
|
365
|
+
and stored on Google servers for 2 days.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
prompt: Cinematic description of the video — include camera movement, lighting,
|
|
369
|
+
subject action, and mood for best results.
|
|
370
|
+
duration_seconds: Video duration in seconds (typically 5–8).
|
|
371
|
+
aspect_ratio: '16:9' for landscape or '9:16' for portrait/mobile.
|
|
372
|
+
negative_prompt: What to exclude from the video.
|
|
373
|
+
anchor_frame_path: Optional path to an image to use as the first frame.
|
|
374
|
+
enhance_prompt: Whether to allow Veo to rewrite/enhance your prompt.
|
|
375
|
+
output_dir: Override the output directory (default: ~/google_flow_outputs).
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Paths or URIs to the generated video(s).
|
|
379
|
+
"""
|
|
380
|
+
client = get_client()
|
|
381
|
+
out = Path(output_dir) if output_dir else _output_dir()
|
|
382
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
383
|
+
|
|
384
|
+
config = types.GenerateVideosConfig(
|
|
385
|
+
duration_seconds=duration_seconds,
|
|
386
|
+
aspect_ratio=aspect_ratio,
|
|
387
|
+
negative_prompt=negative_prompt,
|
|
388
|
+
enhance_prompt=enhance_prompt,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
kwargs: dict = dict(model=MODEL_VIDEO, prompt=prompt, config=config)
|
|
392
|
+
|
|
393
|
+
if anchor_frame_path:
|
|
394
|
+
kwargs["image"] = types.Image(
|
|
395
|
+
image_bytes=_load_image_bytes(anchor_frame_path),
|
|
396
|
+
mime_type=_mime(anchor_frame_path),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
operation = await client.aio.models.generate_videos(**kwargs)
|
|
400
|
+
operation = await _poll_operation(client, operation)
|
|
401
|
+
|
|
402
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
403
|
+
saved, uris = _collect_video_results(operation, out, ts)
|
|
404
|
+
return _format_video_result(saved, uris)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ── Tool 5: Extend Video ──────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@mcp.tool()
|
|
411
|
+
async def flow_extend_video(
|
|
412
|
+
video_path: str,
|
|
413
|
+
prompt: str,
|
|
414
|
+
duration_seconds: int = 8,
|
|
415
|
+
aspect_ratio: str = "16:9",
|
|
416
|
+
output_dir: Optional[str] = None,
|
|
417
|
+
) -> str:
|
|
418
|
+
"""
|
|
419
|
+
Extend an existing Veo 3.1 video clip with additional generated content.
|
|
420
|
+
|
|
421
|
+
Requires a paid Google AI API plan. The extended video is SynthID-watermarked.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
video_path: Absolute path to the source MP4 video file to extend.
|
|
425
|
+
prompt: Description of how to continue the video after the source clip ends.
|
|
426
|
+
duration_seconds: Duration of the generated extension in seconds (typically 5–8).
|
|
427
|
+
aspect_ratio: Must match the source video — '16:9' or '9:16'.
|
|
428
|
+
output_dir: Override the output directory (default: ~/google_flow_outputs).
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Path or URI to the extended video.
|
|
432
|
+
"""
|
|
433
|
+
client = get_client()
|
|
434
|
+
out = Path(output_dir) if output_dir else _output_dir()
|
|
435
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
436
|
+
|
|
437
|
+
with open(video_path, "rb") as f:
|
|
438
|
+
video_bytes = f.read()
|
|
439
|
+
|
|
440
|
+
operation = await client.aio.models.generate_videos(
|
|
441
|
+
model=MODEL_VIDEO,
|
|
442
|
+
prompt=prompt,
|
|
443
|
+
video=types.Video(video_bytes=video_bytes, mime_type="video/mp4"),
|
|
444
|
+
config=types.GenerateVideosConfig(
|
|
445
|
+
duration_seconds=duration_seconds,
|
|
446
|
+
aspect_ratio=aspect_ratio,
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
operation = await _poll_operation(client, operation)
|
|
450
|
+
|
|
451
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
452
|
+
saved, uris = _collect_video_results(operation, out, ts)
|
|
453
|
+
return _format_video_result(saved, uris)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ── Tool 6: Image → Video Pipeline ───────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@mcp.tool()
|
|
460
|
+
async def flow_image_to_video(
|
|
461
|
+
video_prompt: str,
|
|
462
|
+
image_prompt: Optional[str] = None,
|
|
463
|
+
image_path: Optional[str] = None,
|
|
464
|
+
image_model: str = "pro",
|
|
465
|
+
duration_seconds: int = 8,
|
|
466
|
+
aspect_ratio: str = "16:9",
|
|
467
|
+
negative_prompt: Optional[str] = None,
|
|
468
|
+
output_dir: Optional[str] = None,
|
|
469
|
+
) -> str:
|
|
470
|
+
"""
|
|
471
|
+
Full pipeline: generate or use an image with Nano Banana Pro, then animate it with Veo 3.1.
|
|
472
|
+
|
|
473
|
+
Provide either image_prompt (to generate a new anchor image) or image_path (to use an
|
|
474
|
+
existing image). The anchor image becomes the first frame of the video.
|
|
475
|
+
|
|
476
|
+
Requires a paid Google AI API plan for the video step.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
video_prompt: Cinematic description of the video animation — how the scene moves.
|
|
480
|
+
image_prompt: Text prompt to generate the anchor image (if image_path is not given).
|
|
481
|
+
image_path: Path to an existing image to use as the anchor frame.
|
|
482
|
+
image_model: 'pro' (Nano Banana Pro, recommended) or 'flash' (Nano Banana 2).
|
|
483
|
+
duration_seconds: Video duration in seconds (typically 5–8).
|
|
484
|
+
aspect_ratio: '16:9' for landscape or '9:16' for portrait.
|
|
485
|
+
negative_prompt: What to exclude from both the image and video.
|
|
486
|
+
output_dir: Override the output directory (default: ~/google_flow_outputs).
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Paths to the generated anchor image and the final video.
|
|
490
|
+
"""
|
|
491
|
+
if not image_prompt and not image_path:
|
|
492
|
+
raise ValueError("Provide either image_prompt or image_path.")
|
|
493
|
+
|
|
494
|
+
client = get_client()
|
|
495
|
+
out = Path(output_dir) if output_dir else _output_dir()
|
|
496
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
results: list[str] = []
|
|
498
|
+
|
|
499
|
+
# Step 1 — Generate or load the anchor image
|
|
500
|
+
if image_path:
|
|
501
|
+
anchor_path = image_path
|
|
502
|
+
else:
|
|
503
|
+
model_id = _resolve_image_model(image_model)
|
|
504
|
+
img_response = await client.aio.models.generate_images(
|
|
505
|
+
model=model_id,
|
|
506
|
+
prompt=image_prompt,
|
|
507
|
+
config=types.GenerateImagesConfig(
|
|
508
|
+
number_of_images=1,
|
|
509
|
+
aspect_ratio=aspect_ratio,
|
|
510
|
+
negative_prompt=negative_prompt,
|
|
511
|
+
),
|
|
512
|
+
)
|
|
513
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
514
|
+
anchor = out / f"flow_anchor_{ts}.png"
|
|
515
|
+
_save_image(img_response.generated_images[0].image, anchor)
|
|
516
|
+
anchor_path = str(anchor)
|
|
517
|
+
results.append(f"Anchor image:\n{anchor_path}")
|
|
518
|
+
|
|
519
|
+
# Step 2 — Animate with Veo 3.1
|
|
520
|
+
operation = await client.aio.models.generate_videos(
|
|
521
|
+
model=MODEL_VIDEO,
|
|
522
|
+
prompt=video_prompt,
|
|
523
|
+
image=types.Image(
|
|
524
|
+
image_bytes=_load_image_bytes(anchor_path),
|
|
525
|
+
mime_type=_mime(anchor_path),
|
|
526
|
+
),
|
|
527
|
+
config=types.GenerateVideosConfig(
|
|
528
|
+
duration_seconds=duration_seconds,
|
|
529
|
+
aspect_ratio=aspect_ratio,
|
|
530
|
+
negative_prompt=negative_prompt,
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
operation = await _poll_operation(client, operation)
|
|
534
|
+
|
|
535
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
536
|
+
saved, uris = _collect_video_results(operation, out, ts)
|
|
537
|
+
results.append(_format_video_result(saved, uris))
|
|
538
|
+
|
|
539
|
+
return "\n\n".join(results)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ── Entry Point ───────────────────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def main() -> None:
|
|
546
|
+
"""Entry point for uvx / python -m google_flow_mcp."""
|
|
547
|
+
mcp.run(transport="stdio")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
if __name__ == "__main__":
|
|
551
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: google-flow-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server giving Claude Desktop access to Google AI image and video generation
|
|
5
|
+
Project-URL: Homepage, https://github.com/joshuadaniel-8090/google-flow-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/joshuadaniel-8090/google-flow-mcp/issues
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: claude,google-ai,image-generation,imagen,mcp,veo,video-generation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: google-genai>=1.0.0
|
|
19
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
20
|
+
Requires-Dist: pillow>=10.0.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Google Flow MCP
|
|
28
|
+
|
|
29
|
+
A Python MCP server that gives **Claude Desktop** direct access to Google AI's latest image and video generation models.
|
|
30
|
+
|
|
31
|
+
| Capability | Model | Tier |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| High-quality image generation & editing | **Nano Banana Pro** (`gemini-3-pro-image`) | Free |
|
|
34
|
+
| Fast image generation | **Nano Banana 2** (`gemini-3.1-flash-image`) | Free |
|
|
35
|
+
| Cinematic video with native audio | **Veo 3.1** (`veo-3.1-generate-preview`) | Paid |
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | What it does |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `flow_generate_image` | Text → 1–4 images, up to 4K, choice of model |
|
|
42
|
+
| `flow_edit_image` | Edit an image with natural language (inpaint, outpaint, bg-swap) |
|
|
43
|
+
| `flow_generate_image_with_references` | Generate guided by up to 14 reference images |
|
|
44
|
+
| `flow_generate_video` | Text → cinematic video, optional anchor frame |
|
|
45
|
+
| `flow_extend_video` | Extend an existing Veo clip |
|
|
46
|
+
| `flow_image_to_video` | Full pipeline: Nano Banana Pro image → Veo 3.1 video |
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
- Python 3.10 or newer
|
|
51
|
+
- [`uv`](https://docs.astral.sh/uv/getting-started/installation/) (recommended) or `pip`
|
|
52
|
+
- A Google AI Studio API key — get one free at [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Using uv (recommended — no virtual env setup needed)
|
|
58
|
+
uvx google-flow-mcp
|
|
59
|
+
|
|
60
|
+
# Or install with pip
|
|
61
|
+
pip install google-flow-mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Claude Desktop Configuration
|
|
65
|
+
|
|
66
|
+
Add the following to your Claude Desktop config file:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"google-flow": {
|
|
72
|
+
"command": "uvx",
|
|
73
|
+
"args": ["google-flow-mcp"],
|
|
74
|
+
"env": {
|
|
75
|
+
"GOOGLE_API_KEY": "YOUR_GOOGLE_AI_STUDIO_KEY"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Config file locations
|
|
83
|
+
|
|
84
|
+
| Platform | Path |
|
|
85
|
+
|---|---|
|
|
86
|
+
| **Windows Store** | `%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json` |
|
|
87
|
+
| **Windows Direct** | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
88
|
+
| **macOS** | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
89
|
+
| **Linux** | `~/.config/Claude/claude_desktop_config.json` |
|
|
90
|
+
|
|
91
|
+
After editing the config, **restart Claude Desktop**.
|
|
92
|
+
|
|
93
|
+
## Output Directory
|
|
94
|
+
|
|
95
|
+
Generated files are saved to `~/google_flow_outputs/` by default.
|
|
96
|
+
|
|
97
|
+
Override with the `FLOW_OUTPUT_DIR` environment variable:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
"env": {
|
|
101
|
+
"GOOGLE_API_KEY": "YOUR_KEY",
|
|
102
|
+
"FLOW_OUTPUT_DIR": "/Users/you/Pictures/ai-outputs"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Usage Examples
|
|
107
|
+
|
|
108
|
+
Once connected, ask Claude naturally:
|
|
109
|
+
|
|
110
|
+
- *"Generate a photo-realistic image of a neon-lit Tokyo alley at night"*
|
|
111
|
+
- *"Edit this image to remove the background and replace it with a forest"*
|
|
112
|
+
- *"Generate a video of a rocket launching from a desert at dusk with dramatic audio"*
|
|
113
|
+
- *"Create an anchor image of ocean waves, then animate it into a video"*
|
|
114
|
+
|
|
115
|
+
## Important Notes
|
|
116
|
+
|
|
117
|
+
- **Image generation** (Nano Banana Pro / Nano Banana 2) works on the **free tier**.
|
|
118
|
+
- **Video generation** (Veo 3.1) requires a **paid Google AI API plan**.
|
|
119
|
+
- All Veo videos are **SynthID-watermarked** by Google.
|
|
120
|
+
- Generated videos are stored on Google's servers for **2 days** after creation.
|
|
121
|
+
- Video generation typically takes **1–4 minutes** — Claude will wait automatically.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
git clone https://github.com/joshuadaniel-8090/google-flow-mcp
|
|
127
|
+
cd google-flow-mcp
|
|
128
|
+
pip install -e ".[dev]"
|
|
129
|
+
pytest -v
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Apache 2.0 — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
google_flow_mcp/__init__.py,sha256=AoD7BSztEhb9Ax3meNHzCYQ_d8MGwfJHDayVXPowaN4,109
|
|
2
|
+
google_flow_mcp/server.py,sha256=I3ZL1jhCh7rHKVb1MXP_Eq_aaqAiwXLHHF985EFTcZo,20827
|
|
3
|
+
google_flow_mcp-0.1.0.dist-info/METADATA,sha256=bw7aQVOUBa5ylxrlHEKCvJAwLetFYypoWva-xXCPyq8,4442
|
|
4
|
+
google_flow_mcp-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
google_flow_mcp-0.1.0.dist-info/entry_points.txt,sha256=NVy7BAdyKxJFQ1i2Q5t7gJsT41m-KXe7VPHevtUcNgI,64
|
|
6
|
+
google_flow_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=BKz72VQYbcLHoRWoK5N0taW9Y9QiPTACijwnYUF5WV8,7429
|
|
7
|
+
google_flow_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship made available under
|
|
36
|
+
the License, as indicated by a copyright notice that is included in
|
|
37
|
+
or attached to the work (an example is provided in the Appendix below).
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
43
|
+
of this License, Derivative Works shall not include works that remain
|
|
44
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
45
|
+
the Work and Derivative Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean, as submitted to the Licensor for inclusion
|
|
48
|
+
in the Work by the copyright owner or by an individual or Legal Entity
|
|
49
|
+
authorized to submit on behalf of the copyright owner.
|
|
50
|
+
|
|
51
|
+
"Contributor" shall mean Licensor and any Legal Entity on behalf of
|
|
52
|
+
whom a Contribution has been received by the Licensor and included
|
|
53
|
+
within the Work.
|
|
54
|
+
|
|
55
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
56
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
57
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
58
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
59
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
60
|
+
Work and such Derivative Works in Source or Object form.
|
|
61
|
+
|
|
62
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
63
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
64
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
65
|
+
(except as stated in this section) patent license to make, have made,
|
|
66
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
67
|
+
where such license applies only to those patent claims licensable
|
|
68
|
+
by such Contributor that are necessarily infringed by their
|
|
69
|
+
Contribution(s) alone or by the combination of their Contribution(s)
|
|
70
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
71
|
+
institute patent litigation against any entity (including a cross-claim
|
|
72
|
+
or counterclaim in a lawsuit) alleging that the Work or any Contribution
|
|
73
|
+
embodied within the Work constitutes direct or contributory patent
|
|
74
|
+
infringement, then any patent licenses granted to You under this License
|
|
75
|
+
for that Work shall terminate as of the date such litigation is filed.
|
|
76
|
+
|
|
77
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
78
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
79
|
+
modifications, and in Source or Object form, provided that You
|
|
80
|
+
meet the following conditions:
|
|
81
|
+
|
|
82
|
+
(a) You must give any other recipients of the Work or Derivative Works
|
|
83
|
+
a copy of this License; and
|
|
84
|
+
|
|
85
|
+
(b) You must cause any modified files to carry prominent notices
|
|
86
|
+
stating that You changed the files; and
|
|
87
|
+
|
|
88
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
89
|
+
that You distribute, all copyright, patent, trademark, and
|
|
90
|
+
attribution notices from the Source form of the Work; and
|
|
91
|
+
|
|
92
|
+
(d) If the Work includes a "NOTICE" text file, you must include a
|
|
93
|
+
readable copy of the attribution notices contained within such
|
|
94
|
+
NOTICE file.
|
|
95
|
+
|
|
96
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
97
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
98
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
99
|
+
this License, without any additional terms or conditions.
|
|
100
|
+
|
|
101
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
102
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
103
|
+
|
|
104
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed
|
|
105
|
+
to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
106
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
107
|
+
implied. You are solely responsible for determining the
|
|
108
|
+
appropriateness of using or reproducing the Work.
|
|
109
|
+
|
|
110
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
111
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
112
|
+
unless required by applicable law (such as deliberate and grossly
|
|
113
|
+
negligent acts) shall any Contributor be liable to You for damages,
|
|
114
|
+
including any direct, indirect, special, incidental, or exemplary
|
|
115
|
+
damages of any character arising as a result of this License or out
|
|
116
|
+
of the use or inability to use the Work.
|
|
117
|
+
|
|
118
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
119
|
+
the Work or Derivative Works thereof, You may choose to offer, and
|
|
120
|
+
charge a fee for, acceptance of support, warranty, indemnity, or
|
|
121
|
+
other liability obligations and/or rights consistent with this
|
|
122
|
+
License. However, in accepting such obligations, You may offer such
|
|
123
|
+
obligations only on Your own behalf and on Your sole responsibility.
|
|
124
|
+
|
|
125
|
+
END OF TERMS AND CONDITIONS
|
|
126
|
+
|
|
127
|
+
Copyright 2025 Joshua Daniel
|
|
128
|
+
|
|
129
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
130
|
+
you may not use this file except in compliance with the License.
|
|
131
|
+
You may obtain a copy of the License at
|
|
132
|
+
|
|
133
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
134
|
+
|
|
135
|
+
Unless required by applicable law or agreed to in writing, software
|
|
136
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
137
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
138
|
+
See the License for the specific language governing permissions and
|
|
139
|
+
limitations under the License.
|