iapp-mcp 2.0.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.
iapp_mcp/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """iApp AI Marketplace MCP server."""
2
+
3
+ __version__ = "2.0.0"
iapp_mcp/app.py ADDED
@@ -0,0 +1,15 @@
1
+ """FastMCP application instance shared by all tool modules."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ mcp = FastMCP(
6
+ "iapp_ai",
7
+ instructions=(
8
+ "MCP server for the iApp AI Marketplace (https://iapp.co.th) — Thai-focused AI APIs: "
9
+ "OCR (documents, receipts, ID cards), eKYC (face verification/liveness), Thai NLP "
10
+ "(translation, summarization, sentiment, QA), LLM chat (Chinda Thai LLM, DeepSeek), "
11
+ "speech-to-text, text-to-speech, image/video generation, and smart-city utilities. "
12
+ "Requires the IAPP_API_KEY environment variable. Get an API key at https://iapp.co.th. "
13
+ "Most tools accept local file paths for images/documents/audio and consume iApp credits (IC) per call."
14
+ ),
15
+ )
iapp_mcp/client.py ADDED
@@ -0,0 +1,31 @@
1
+ """Backwards-compatible shim over :mod:`iapp_core`.
2
+
3
+ The HTTP client, error type and formatting helpers now live in the shared
4
+ ``iapp_core`` package so the SDK and the MCP server have a single source of
5
+ truth. This module re-exports them under the names the tool modules already
6
+ import, so ``iapp_mcp.tools.*`` need no changes:
7
+
8
+ from ..client import IAppAPIError, format_json_response, request, save_binary, save_pcm_as_wav
9
+ """
10
+
11
+ from iapp_core.config import API_BASE
12
+ from iapp_core.errors import IAppAPIError
13
+ from iapp_core.formatting import (
14
+ format_json_response,
15
+ resolve_input_file,
16
+ resolve_output_path,
17
+ save_binary,
18
+ save_pcm_as_wav,
19
+ )
20
+ from iapp_core.transport import request_async as request
21
+
22
+ __all__ = [
23
+ "API_BASE",
24
+ "IAppAPIError",
25
+ "format_json_response",
26
+ "request",
27
+ "resolve_input_file",
28
+ "resolve_output_path",
29
+ "save_binary",
30
+ "save_pcm_as_wav",
31
+ ]
iapp_mcp/server.py ADDED
@@ -0,0 +1,15 @@
1
+ """Entry point for the iApp AI Marketplace MCP server."""
2
+
3
+ from .app import mcp
4
+
5
+ # Importing tool modules registers their tools on the shared FastMCP instance.
6
+ from .tools import ekyc, generation, llm, nlp, ocr, smartcity, speech # noqa: F401, E402
7
+
8
+
9
+ def main() -> None:
10
+ """Run the MCP server over stdio."""
11
+ mcp.run()
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
@@ -0,0 +1 @@
1
+ """iApp MCP tool modules."""
iapp_mcp/tools/ekyc.py ADDED
@@ -0,0 +1,393 @@
1
+ """eKYC tools: Thai ID card, passport, driver license, book bank, face APIs."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from ..app import mcp
6
+ from ..client import IAppAPIError, format_json_response, request
7
+
8
+
9
+ @mcp.tool(
10
+ name="iapp_thai_id_card_ocr",
11
+ annotations={
12
+ "title": "Thai National ID Card OCR",
13
+ "readOnlyHint": True,
14
+ "destructiveHint": False,
15
+ "idempotentHint": True,
16
+ "openWorldHint": True,
17
+ },
18
+ )
19
+ async def iapp_thai_id_card_ocr(
20
+ file_path: str,
21
+ side: Literal["front", "back"] = "front",
22
+ options: Optional[str] = None,
23
+ ) -> str:
24
+ """Extract structured data from a Thai national ID card image (front or back).
25
+
26
+ Front side returns ID number, Thai/English name, date of birth, address, religion,
27
+ issue/expiry dates, gender and per-field confidence scores. Back side returns the
28
+ laser code (back_number).
29
+
30
+ Args:
31
+ file_path: Local path to the card image (JPEG/PNG/HEIC/PDF, max 10MB).
32
+ side: Which side of the card the image shows ('front' or 'back').
33
+ options: Optional comma-separated flags: not_crop_card, not_rotate_card,
34
+ get_bbox, get_image, get_original.
35
+
36
+ Returns:
37
+ JSON string with extracted fields and confidence scores.
38
+ Cost: 1.25 IC (front) / 0.75 IC (back).
39
+ """
40
+ try:
41
+ data = {"options": options} if options else None
42
+ response = await request(
43
+ "POST",
44
+ f"/v3/store/ekyc/thai-national-id-card/{side}",
45
+ data=data,
46
+ file_fields=[("file", file_path)],
47
+ )
48
+ return format_json_response(response)
49
+ except IAppAPIError as e:
50
+ return str(e)
51
+
52
+
53
+ @mcp.tool(
54
+ name="iapp_thai_id_card_photocopy_ocr",
55
+ annotations={
56
+ "title": "Thai ID Card (Photocopy) OCR with Signature Detection",
57
+ "readOnlyHint": True,
58
+ "destructiveHint": False,
59
+ "idempotentHint": True,
60
+ "openWorldHint": True,
61
+ },
62
+ )
63
+ async def iapp_thai_id_card_photocopy_ocr(file_path: str) -> str:
64
+ """Extract data from a photographed/photocopied Thai national ID card, including signature detection.
65
+
66
+ Use this for photocopies of ID cards (common in Thai KYC paperwork) instead of
67
+ iapp_thai_id_card_ocr, which expects the physical card.
68
+
69
+ Args:
70
+ file_path: Local path to the photocopied card image (JPEG/PNG/HEIC, max 2MB,
71
+ min 600x400 px).
72
+
73
+ Returns:
74
+ JSON string with national_id, names (TH/EN), date of birth/expiry, address fields,
75
+ signature detection reason codes, and confidence scores. Cost: 1.25 IC.
76
+ """
77
+ try:
78
+ response = await request(
79
+ "POST",
80
+ "/v3/store/ekyc/thai-national-id-card-with-signature",
81
+ file_fields=[("file", file_path)],
82
+ )
83
+ return format_json_response(response)
84
+ except IAppAPIError as e:
85
+ return str(e)
86
+
87
+
88
+ @mcp.tool(
89
+ name="iapp_passport_ocr",
90
+ annotations={
91
+ "title": "Passport OCR",
92
+ "readOnlyHint": True,
93
+ "destructiveHint": False,
94
+ "idempotentHint": True,
95
+ "openWorldHint": True,
96
+ },
97
+ )
98
+ async def iapp_passport_ocr(file_path: str, segmentation: bool = False) -> str:
99
+ """Extract MRZ and personal data from a passport image (any country).
100
+
101
+ Args:
102
+ file_path: Local path to the passport image (JPEG/PNG/PDF, max 10MB).
103
+ segmentation: Set True to handle skewed/angled photos.
104
+
105
+ Returns:
106
+ JSON string with passport number, names, nationality, dates, MRZ checksum
107
+ validity flags and raw MRZ text. Cost: 0.75 IC per page.
108
+ """
109
+ try:
110
+ data = {"options": "segmentation"} if segmentation else None
111
+ response = await request(
112
+ "POST",
113
+ "/v3/store/ekyc/passport",
114
+ data=data,
115
+ file_fields=[("file", file_path)],
116
+ )
117
+ return format_json_response(response)
118
+ except IAppAPIError as e:
119
+ return str(e)
120
+
121
+
122
+ @mcp.tool(
123
+ name="iapp_thai_driver_license_ocr",
124
+ annotations={
125
+ "title": "Thai Driver License OCR",
126
+ "readOnlyHint": True,
127
+ "destructiveHint": False,
128
+ "idempotentHint": True,
129
+ "openWorldHint": True,
130
+ },
131
+ )
132
+ async def iapp_thai_driver_license_ocr(file_path: str) -> str:
133
+ """Extract structured data from a Thai driver license card image.
134
+
135
+ Args:
136
+ file_path: Local path to the license image (JPEG/PNG, max 10MB).
137
+
138
+ Returns:
139
+ JSON string with license number, Thai/English name, date of birth, license type,
140
+ issue/expiry dates. Cost: 1.25 IC.
141
+ """
142
+ try:
143
+ response = await request(
144
+ "POST",
145
+ "/v3/store/ekyc/thai-driver-license",
146
+ file_fields=[("file", file_path)],
147
+ )
148
+ return format_json_response(response)
149
+ except IAppAPIError as e:
150
+ return str(e)
151
+
152
+
153
+ @mcp.tool(
154
+ name="iapp_book_bank_ocr",
155
+ annotations={
156
+ "title": "Thai Bank Book OCR",
157
+ "readOnlyHint": True,
158
+ "destructiveHint": False,
159
+ "idempotentHint": True,
160
+ "openWorldHint": True,
161
+ },
162
+ )
163
+ async def iapp_book_bank_ocr(file_path: str) -> str:
164
+ """Extract account details from a Thai bank book (passbook) cover image.
165
+
166
+ Args:
167
+ file_path: Local path to the bank book image (JPEG/PNG).
168
+
169
+ Returns:
170
+ JSON string with bank name, account number, account name, branch and
171
+ signature detection. Cost: 1.25 IC.
172
+ """
173
+ try:
174
+ response = await request(
175
+ "POST",
176
+ "/v3/store/ekyc/book-bank",
177
+ file_fields=[("file", file_path)],
178
+ )
179
+ return format_json_response(response)
180
+ except IAppAPIError as e:
181
+ return str(e)
182
+
183
+
184
+ @mcp.tool(
185
+ name="iapp_face_verification",
186
+ annotations={
187
+ "title": "Face Verification (1:1 Compare)",
188
+ "readOnlyHint": True,
189
+ "destructiveHint": False,
190
+ "idempotentHint": True,
191
+ "openWorldHint": True,
192
+ },
193
+ )
194
+ async def iapp_face_verification(
195
+ image1_path: str,
196
+ image2_path: str,
197
+ threshold: Optional[float] = None,
198
+ ) -> str:
199
+ """Compare two face images and check whether they are the same person (1:1 verification).
200
+
201
+ Args:
202
+ image1_path: Local path to the first face image (JPG/PNG/HEIC, max 2MB, min 600x400).
203
+ image2_path: Local path to the second face image (same constraints).
204
+ threshold: Match threshold. Default 36; use 48 for high precision.
205
+
206
+ Returns:
207
+ JSON string with matched (boolean), score, threshold and duration. Cost: 0.3 IC.
208
+ """
209
+ try:
210
+ data = {"threshold": threshold} if threshold is not None else None
211
+ response = await request(
212
+ "POST",
213
+ "/v3/store/ekyc/face-verification",
214
+ data=data,
215
+ file_fields=[("file1", image1_path), ("file2", image2_path)],
216
+ )
217
+ return format_json_response(response)
218
+ except IAppAPIError as e:
219
+ return str(e)
220
+
221
+
222
+ @mcp.tool(
223
+ name="iapp_face_detection",
224
+ annotations={
225
+ "title": "Face Detection",
226
+ "readOnlyHint": True,
227
+ "destructiveHint": False,
228
+ "idempotentHint": True,
229
+ "openWorldHint": True,
230
+ },
231
+ )
232
+ async def iapp_face_detection(
233
+ file_path: str,
234
+ mode: Literal["single", "multi"] = "single",
235
+ ) -> str:
236
+ """Detect face(s) in an image and return bounding boxes and detection scores.
237
+
238
+ Args:
239
+ file_path: Local path to the image (JPEG/PNG, max 10MB).
240
+ mode: 'single' expects exactly one face; 'multi' detects all faces.
241
+
242
+ Returns:
243
+ JSON string with bbox coordinates, detection_score and cropped face (base64,
244
+ truncated in output). Cost: 0.2-0.3 IC.
245
+ """
246
+ try:
247
+ response = await request(
248
+ "POST",
249
+ f"/v3/store/ekyc/face-detection/{mode}",
250
+ file_fields=[("file", file_path)],
251
+ )
252
+ return format_json_response(response)
253
+ except IAppAPIError as e:
254
+ return str(e)
255
+
256
+
257
+ @mcp.tool(
258
+ name="iapp_face_liveness",
259
+ annotations={
260
+ "title": "Face Passive Liveness Detection",
261
+ "readOnlyHint": True,
262
+ "destructiveHint": False,
263
+ "idempotentHint": True,
264
+ "openWorldHint": True,
265
+ },
266
+ )
267
+ async def iapp_face_liveness(file_path: str) -> str:
268
+ """Check whether a face photo is of a real live person or a spoof (screen/printout).
269
+
270
+ iBeta Level 1 certified passive liveness detection — no user gestures required.
271
+
272
+ Args:
273
+ file_path: Local path to the face image (JPEG/PNG, max 10MB).
274
+
275
+ Returns:
276
+ JSON string with predict ('REAL' or 'SPOOF'), score (0-1) and darkness metric.
277
+ Cost: 0.3 IC.
278
+ """
279
+ try:
280
+ response = await request(
281
+ "POST",
282
+ "/v3/store/ekyc/face-passive-liveness",
283
+ file_fields=[("file", file_path)],
284
+ )
285
+ return format_json_response(response)
286
+ except IAppAPIError as e:
287
+ return str(e)
288
+
289
+
290
+ @mcp.tool(
291
+ name="iapp_face_id_card_kyc",
292
+ annotations={
293
+ "title": "Face + ID Card KYC Verification",
294
+ "readOnlyHint": True,
295
+ "destructiveHint": False,
296
+ "idempotentHint": True,
297
+ "openWorldHint": True,
298
+ },
299
+ )
300
+ async def iapp_face_id_card_kyc(id_card_path: str, selfie_path: str) -> str:
301
+ """Verify that a selfie matches the face photo on a Thai national ID card (full KYC check).
302
+
303
+ Args:
304
+ id_card_path: Local path to the ID card image (JPG/PNG, min 600x400, max 10MB).
305
+ selfie_path: Local path to the selfie image (same constraints).
306
+
307
+ Returns:
308
+ JSON string with per-image and total confidence plus isSamePerson verdicts.
309
+ Cost: 1 IC.
310
+ """
311
+ try:
312
+ response = await request(
313
+ "POST",
314
+ "/v3/store/ekyc/face-and-id-card-verification",
315
+ file_fields=[("file0", id_card_path), ("file1", selfie_path)],
316
+ )
317
+ return format_json_response(response)
318
+ except IAppAPIError as e:
319
+ return str(e)
320
+
321
+
322
+ @mcp.tool(
323
+ name="iapp_face_recognition",
324
+ annotations={
325
+ "title": "Face Recognition (1:N Search / Enroll)",
326
+ "readOnlyHint": False,
327
+ "destructiveHint": True,
328
+ "idempotentHint": False,
329
+ "openWorldHint": True,
330
+ },
331
+ )
332
+ async def iapp_face_recognition(
333
+ action: Literal["recognize_single", "recognize_multi", "add", "remove", "check"],
334
+ company: str,
335
+ file_path: Optional[str] = None,
336
+ name: Optional[str] = None,
337
+ password: Optional[str] = None,
338
+ face_id: Optional[str] = None,
339
+ ) -> str:
340
+ """Search, enroll or manage faces in a company face database (1:N recognition).
341
+
342
+ Actions:
343
+ - recognize_single: identify one face in the image (requires file_path)
344
+ - recognize_multi: identify all faces in the image (requires file_path)
345
+ - add: enroll a face (requires file_path, name, password) — modifies the database
346
+ - remove: delete an enrolled face (requires name, password; optional face_id) — destructive
347
+ - check: list enrolled faces (requires password)
348
+
349
+ Args:
350
+ action: Operation to perform.
351
+ company: Company namespace of the face database.
352
+ file_path: Local path to a face image (JPEG/PNG, max 2MB) for recognize/add.
353
+ name: Person name for add/remove.
354
+ password: Company password for add/remove/check.
355
+ face_id: Specific face ID to remove (optional).
356
+
357
+ Returns:
358
+ JSON string with recognition matches/scores or operation status.
359
+ Cost: 0.3 IC (recognize/check), 0.1 IC (add), 0 IC (remove).
360
+ """
361
+ try:
362
+ endpoint_map = {
363
+ "recognize_single": "/v3/store/ekyc/face-recognition/single",
364
+ "recognize_multi": "/v3/store/ekyc/face-recognition/multi",
365
+ "add": "/v3/store/ekyc/face-recognition/add",
366
+ "remove": "/v3/store/ekyc/face-recognition/remove",
367
+ "check": "/v3/store/ekyc/face-recognition/check",
368
+ }
369
+ data = {"company": company}
370
+ if name:
371
+ data["name"] = name
372
+ if password:
373
+ data["password"] = password
374
+ if face_id:
375
+ data["face_id"] = face_id
376
+
377
+ needs_file = action in ("recognize_single", "recognize_multi", "add")
378
+ if needs_file and not file_path:
379
+ return f"Error: action '{action}' requires file_path."
380
+ if action in ("add", "remove", "check") and not password:
381
+ return f"Error: action '{action}' requires password."
382
+ if action in ("add", "remove") and not name:
383
+ return f"Error: action '{action}' requires name."
384
+
385
+ response = await request(
386
+ "POST",
387
+ endpoint_map[action],
388
+ data=data,
389
+ file_fields=[("file", file_path)] if needs_file else None,
390
+ )
391
+ return format_json_response(response)
392
+ except IAppAPIError as e:
393
+ return str(e)
@@ -0,0 +1,216 @@
1
+ """Image and video generation tools: Nano Banana, background removal, Seedance video."""
2
+
3
+ import base64
4
+ from typing import Literal, Optional
5
+
6
+ from ..app import mcp
7
+ from ..client import (
8
+ IAppAPIError,
9
+ format_json_response,
10
+ request,
11
+ save_binary,
12
+ )
13
+
14
+ _VIDEO_MODELS = {
15
+ "seedance": "dreamina-seedance-2-0-260128",
16
+ "seedance-fast": "dreamina-seedance-2-0-fast-260128",
17
+ }
18
+
19
+
20
+ @mcp.tool(
21
+ name="iapp_image_generation",
22
+ annotations={
23
+ "title": "Image Generation (Google Nano Banana)",
24
+ "readOnlyHint": False,
25
+ "destructiveHint": False,
26
+ "idempotentHint": False,
27
+ "openWorldHint": True,
28
+ },
29
+ )
30
+ async def iapp_image_generation(
31
+ prompt: str,
32
+ output_path: str,
33
+ model: Literal["nanobanana", "nanobanana-pro"] = "nanobanana",
34
+ ) -> str:
35
+ """Generate an image from a text prompt using Google Nano Banana via iApp and save it locally.
36
+
37
+ Args:
38
+ prompt: Text prompt describing the image (max 32,000 chars).
39
+ output_path: Local path to save the generated image (.png).
40
+ model: 'nanobanana' (Gemini 2.5 Flash Image, 3 IC) or 'nanobanana-pro'
41
+ (Gemini 3 Pro Image, 8 IC).
42
+
43
+ Returns:
44
+ Confirmation message with the saved image path, plus any text the model returned.
45
+ Latency 5-30 seconds.
46
+ """
47
+ # Both routes confirmed correct with the iApp team (verified 2026-06).
48
+ endpoint = (
49
+ "/v3/image/generation/google/nanobanana/generate"
50
+ if model == "nanobanana"
51
+ else "/v3/image/generation/google/nanobananapro/generate"
52
+ )
53
+ try:
54
+ response = await request(
55
+ "POST",
56
+ endpoint,
57
+ json_body={
58
+ "contents": [{"parts": [{"text": prompt}]}],
59
+ "generationConfig": {"responseModalities": ["TEXT", "IMAGE"]},
60
+ },
61
+ )
62
+ payload = response.json()
63
+ saved_path = None
64
+ text_parts = []
65
+ for candidate in payload.get("candidates", []):
66
+ for part in candidate.get("content", {}).get("parts", []):
67
+ inline = part.get("inlineData")
68
+ if inline and inline.get("data") and saved_path is None:
69
+ saved_path = save_binary(base64.b64decode(inline["data"]), output_path)
70
+ elif part.get("text"):
71
+ text_parts.append(part["text"])
72
+ if saved_path is None:
73
+ return f"Error: No image data in response. Raw response: {str(payload)[:500]}"
74
+ result = f"Image saved to {saved_path}"
75
+ if text_parts:
76
+ result += "\n\nModel notes: " + "\n".join(text_parts)
77
+ return result
78
+ except IAppAPIError as e:
79
+ return str(e)
80
+ except (ValueError, KeyError) as e:
81
+ return f"Error: Unexpected image generation response format: {e}"
82
+
83
+
84
+ @mcp.tool(
85
+ name="iapp_remove_background",
86
+ annotations={
87
+ "title": "Image Background Removal",
88
+ "readOnlyHint": False,
89
+ "destructiveHint": False,
90
+ "idempotentHint": True,
91
+ "openWorldHint": True,
92
+ },
93
+ )
94
+ async def iapp_remove_background(file_path: str, output_path: str) -> str:
95
+ """Remove the background from an image and save the result locally.
96
+
97
+ Args:
98
+ file_path: Local path to the input image (PNG/JPEG, max 2MB).
99
+ output_path: Local path to save the background-removed image.
100
+
101
+ Returns:
102
+ Confirmation message with the saved image path. Cost: 1 IC.
103
+ """
104
+ try:
105
+ response = await request(
106
+ "POST",
107
+ "/v3/store/smart-city/remove-background",
108
+ file_fields=[("file", file_path)],
109
+ )
110
+ path = save_binary(response.content, output_path)
111
+ return f"Background-removed image saved to {path} ({len(response.content)} bytes)"
112
+ except IAppAPIError as e:
113
+ return str(e)
114
+
115
+
116
+ @mcp.tool(
117
+ name="iapp_video_generation_submit",
118
+ annotations={
119
+ "title": "Video Generation — Submit Job (Seedance 2.0)",
120
+ "readOnlyHint": False,
121
+ "destructiveHint": False,
122
+ "idempotentHint": False,
123
+ "openWorldHint": True,
124
+ },
125
+ )
126
+ async def iapp_video_generation_submit(
127
+ prompt: str,
128
+ model: Literal["seedance", "seedance-fast"] = "seedance-fast",
129
+ duration: int = 5,
130
+ ratio: Literal["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"] = "16:9",
131
+ resolution: Literal["480p", "720p", "1080p"] = "720p",
132
+ generate_audio: bool = True,
133
+ watermark: bool = False,
134
+ first_frame_image_url: Optional[str] = None,
135
+ reference_image_url: Optional[str] = None,
136
+ ) -> str:
137
+ """Submit an async video generation job (Seedance 2.0). Poll with iapp_video_generation_status.
138
+
139
+ Args:
140
+ prompt: Text description of the video to generate.
141
+ model: 'seedance' (higher quality) or 'seedance-fast' (cheaper/faster).
142
+ duration: Video length in seconds (4-15, default 5).
143
+ ratio: Aspect ratio.
144
+ resolution: Output resolution ('1080p' not available on seedance-fast).
145
+ generate_audio: Whether to generate audio.
146
+ watermark: Whether to add a watermark.
147
+ first_frame_image_url: Optional public image URL to use as the first frame.
148
+ reference_image_url: Optional public image URL to use as a style/content reference.
149
+
150
+ Returns:
151
+ JSON string with the task id — pass it to iapp_video_generation_status.
152
+ Pricing: ~0.14-0.33 IC per 1K output tokens; failed jobs cost 0 IC.
153
+ """
154
+ try:
155
+ content = [{"type": "text", "text": prompt}]
156
+ if first_frame_image_url:
157
+ content.append(
158
+ {
159
+ "type": "image_url",
160
+ "image_url": {"url": first_frame_image_url},
161
+ "role": "first_frame",
162
+ }
163
+ )
164
+ if reference_image_url:
165
+ content.append(
166
+ {
167
+ "type": "image_url",
168
+ "image_url": {"url": reference_image_url},
169
+ "role": "reference_image",
170
+ }
171
+ )
172
+ response = await request(
173
+ "POST",
174
+ "/v3/store/video/seedance/tasks",
175
+ json_body={
176
+ "model": _VIDEO_MODELS[model],
177
+ "content": content,
178
+ "duration": duration,
179
+ "ratio": ratio,
180
+ "resolution": resolution,
181
+ "generate_audio": generate_audio,
182
+ "watermark": watermark,
183
+ },
184
+ )
185
+ return format_json_response(response)
186
+ except IAppAPIError as e:
187
+ return str(e)
188
+
189
+
190
+ @mcp.tool(
191
+ name="iapp_video_generation_status",
192
+ annotations={
193
+ "title": "Video Generation — Check Job Status",
194
+ "readOnlyHint": True,
195
+ "destructiveHint": False,
196
+ "idempotentHint": True,
197
+ "openWorldHint": True,
198
+ },
199
+ )
200
+ async def iapp_video_generation_status(task_id: str) -> str:
201
+ """Check the status of a Seedance video generation job and get the video URL when done.
202
+
203
+ Poll every ~5 seconds. Terminal states: succeeded, failed, cancelled, expired.
204
+
205
+ Args:
206
+ task_id: Task id returned by iapp_video_generation_submit.
207
+
208
+ Returns:
209
+ JSON string with status and, when succeeded, the video download URL
210
+ (URL expires ~24 hours after generation).
211
+ """
212
+ try:
213
+ response = await request("GET", f"/v3/store/video/seedance/tasks/{task_id}")
214
+ return format_json_response(response)
215
+ except IAppAPIError as e:
216
+ return str(e)