muapi-cli 0.2.5__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.
- muapi/__init__.py +1 -0
- muapi/client.py +121 -0
- muapi/commands/__init__.py +0 -0
- muapi/commands/account.py +89 -0
- muapi/commands/audio.py +139 -0
- muapi/commands/auth.py +193 -0
- muapi/commands/config_cmd.py +80 -0
- muapi/commands/docs.py +81 -0
- muapi/commands/edit.py +134 -0
- muapi/commands/enhance.py +157 -0
- muapi/commands/image.py +297 -0
- muapi/commands/keys.py +115 -0
- muapi/commands/mcp_server.py +905 -0
- muapi/commands/models.py +79 -0
- muapi/commands/predict.py +43 -0
- muapi/commands/run.py +173 -0
- muapi/commands/upload.py +31 -0
- muapi/commands/video.py +318 -0
- muapi/commands/workflow.py +746 -0
- muapi/config.py +110 -0
- muapi/dynamic_help.py +144 -0
- muapi/exitcodes.py +14 -0
- muapi/main.py +98 -0
- muapi/schema_introspect.py +175 -0
- muapi/utils.py +202 -0
- muapi_cli-0.2.5.dist-info/METADATA +337 -0
- muapi_cli-0.2.5.dist-info/RECORD +29 -0
- muapi_cli-0.2.5.dist-info/WHEEL +4 -0
- muapi_cli-0.2.5.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
"""muapi mcp serve — expose all muapi tools as an MCP server.
|
|
2
|
+
|
|
3
|
+
Run: muapi mcp serve
|
|
4
|
+
Then add to Claude Desktop / VS Code / any MCP client.
|
|
5
|
+
|
|
6
|
+
Each generation endpoint becomes a structured MCP tool with:
|
|
7
|
+
- Full JSON Schema input definition
|
|
8
|
+
- outputSchema for validated structured responses
|
|
9
|
+
- Proper isError signalling (no silent failures)
|
|
10
|
+
- Tool annotations (read-only vs. side-effecting)
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from .. import client as api_client
|
|
19
|
+
from ..config import get_api_key
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(help="Run muapi as an MCP server for AI agent integration.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Shared schemas ────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
def _prediction_output_schema() -> dict:
|
|
27
|
+
return {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"request_id": {"type": "string"},
|
|
31
|
+
"status": {"type": "string", "enum": ["pending", "processing", "completed", "failed"]},
|
|
32
|
+
"outputs": {"type": "array", "items": {"type": "string", "format": "uri"}},
|
|
33
|
+
"error": {"type": "string"},
|
|
34
|
+
},
|
|
35
|
+
"required": ["status"],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Tool registry ─────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
TOOLS = [
|
|
42
|
+
# ── Images ──────────────────────────────────────────────────────────────
|
|
43
|
+
{
|
|
44
|
+
"name": "muapi_image_generate",
|
|
45
|
+
"description": "Generate an image from a text prompt using muapi.ai. Returns URLs of generated images.",
|
|
46
|
+
"endpoint": None, # dynamic — chosen from 'model' param
|
|
47
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
48
|
+
"inputSchema": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"prompt": {"type": "string", "description": "Text description of the image to generate"},
|
|
52
|
+
"model": {"type": "string", "description": "Model name", "default": "flux-dev",
|
|
53
|
+
"enum": ["flux-dev","flux-schnell","flux-krea",
|
|
54
|
+
"flux-kontext-dev","flux-kontext-pro","flux-kontext-max",
|
|
55
|
+
"flux-2-dev","flux-2-pro","flux-2-flex",
|
|
56
|
+
"flux-2-klein-4b","flux-2-klein-9b",
|
|
57
|
+
"hidream-fast","hidream-dev","hidream-full",
|
|
58
|
+
"wan2.1","wan2.5","wan2.6","wan2.7","wan2.7-pro",
|
|
59
|
+
"gpt4o","gpt-image","gpt-image-2",
|
|
60
|
+
"imagen4","imagen4-fast","imagen4-ultra",
|
|
61
|
+
"midjourney","midjourney-v7","midjourney-v8","midjourney-niji",
|
|
62
|
+
"seedream","seedream-v3","seedream-v4","seedream-v4.5","seedream-5",
|
|
63
|
+
"qwen","qwen-2","qwen-2-pro",
|
|
64
|
+
"nano-banana","nano-banana-pro","nano-banana-2",
|
|
65
|
+
"kling-o1","kling-o3",
|
|
66
|
+
"hunyuan","hunyuan-3","ideogram","reve",
|
|
67
|
+
"z-image","z-image-turbo",
|
|
68
|
+
"leonardo-lucid","leonardo-phoenix",
|
|
69
|
+
"grok","grok-quality","chroma",
|
|
70
|
+
"sdxl","perfect-pony","neta-lumina"]},
|
|
71
|
+
"width": {"type": "integer", "description": "Image width in pixels", "default": 1024},
|
|
72
|
+
"height": {"type": "integer", "description": "Image height in pixels", "default": 1024},
|
|
73
|
+
"num_images": {"type": "integer", "description": "Number of images (1-4)", "default": 1, "minimum": 1, "maximum": 4},
|
|
74
|
+
"aspect_ratio": {"type": "string", "description": "Aspect ratio (used by kontext/midjourney models)", "default": "1:1"},
|
|
75
|
+
},
|
|
76
|
+
"required": ["prompt"],
|
|
77
|
+
},
|
|
78
|
+
"outputSchema": _prediction_output_schema(),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "muapi_image_edit",
|
|
82
|
+
"description": "Edit or transform an image using a text prompt and a source image URL.",
|
|
83
|
+
"endpoint": None,
|
|
84
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
85
|
+
"inputSchema": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"prompt": {"type": "string", "description": "Edit instruction"},
|
|
89
|
+
"image_url": {"type": "string", "description": "Source image URL", "format": "uri"},
|
|
90
|
+
"model": {"type": "string", "default": "flux-kontext-dev",
|
|
91
|
+
"enum": ["flux-kontext-dev","flux-kontext-pro","flux-kontext-max",
|
|
92
|
+
"flux-kontext-effects",
|
|
93
|
+
"flux-2-dev-edit","flux-2-pro-edit","flux-2-flex-edit",
|
|
94
|
+
"flux-2-klein-4b-edit","flux-2-klein-9b-edit",
|
|
95
|
+
"gpt4o","gpt4o-edit","gpt-image-edit","gpt-image-2-edit",
|
|
96
|
+
"reve","seededit",
|
|
97
|
+
"seedream-edit","seedream-v4.5-edit","seedream-5-edit",
|
|
98
|
+
"seedance-character",
|
|
99
|
+
"midjourney","midjourney-style","midjourney-omni",
|
|
100
|
+
"qwen","qwen-plus","qwen-plus-lora","qwen-2511",
|
|
101
|
+
"qwen-2-edit","qwen-2-pro-edit",
|
|
102
|
+
"nano-banana-edit","nano-banana-effects",
|
|
103
|
+
"nano-banana-2-edit","nano-banana-pro-edit",
|
|
104
|
+
"kling-o1-edit","kling-o3-edit",
|
|
105
|
+
"wan2.5-edit","wan2.6-edit","wan2.7-edit","wan2.7-edit-pro",
|
|
106
|
+
"ideogram-character","ideogram-reframe",
|
|
107
|
+
"flux-redux","flux-pulid","grok",
|
|
108
|
+
"photo-pack","portrait-stylist",
|
|
109
|
+
"minimax-subject","vidu-q2-ref"]},
|
|
110
|
+
"aspect_ratio": {"type": "string", "default": "1:1"},
|
|
111
|
+
"num_images": {"type": "integer", "default": 1, "minimum": 1, "maximum": 4},
|
|
112
|
+
},
|
|
113
|
+
"required": ["prompt", "image_url"],
|
|
114
|
+
},
|
|
115
|
+
"outputSchema": _prediction_output_schema(),
|
|
116
|
+
},
|
|
117
|
+
# ── Videos ──────────────────────────────────────────────────────────────
|
|
118
|
+
{
|
|
119
|
+
"name": "muapi_video_generate",
|
|
120
|
+
"description": "Generate a video from a text prompt using muapi.ai.",
|
|
121
|
+
"endpoint": None,
|
|
122
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
123
|
+
"inputSchema": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"properties": {
|
|
126
|
+
"prompt": {"type": "string", "description": "Video description prompt"},
|
|
127
|
+
"model": {"type": "string", "default": "kling-master",
|
|
128
|
+
"enum": ["veo3","veo3-fast","veo3.1","veo3.1-fast","veo3.1-4k",
|
|
129
|
+
"veo3.1-lite","veo4",
|
|
130
|
+
"kling-master","kling-v2.5-pro","kling-v2.6-pro",
|
|
131
|
+
"kling-v3-pro","kling-v3-std","kling-v3-4k",
|
|
132
|
+
"kling-v3-omni","kling-v3-omni-std","kling-v3-omni-4k",
|
|
133
|
+
"kling-o1",
|
|
134
|
+
"wan2.1","wan2.2","wan2.2-5b-fast",
|
|
135
|
+
"wan2.5","wan2.5-fast","wan2.6","wan2.7",
|
|
136
|
+
"seedance-pro","seedance-pro-fast","seedance-lite",
|
|
137
|
+
"seedance-v1.5","seedance-v1.5-fast","seedance-v2",
|
|
138
|
+
"seedance-2","seedance-2-fast",
|
|
139
|
+
"seedance-2-vip","seedance-2-vip-fast",
|
|
140
|
+
"hunyuan","hunyuan-fast","runway",
|
|
141
|
+
"pixverse","pixverse-v4.5","pixverse-v5","pixverse-v5.5","pixverse-v6",
|
|
142
|
+
"vidu","vidu-q2-pro","vidu-q2-turbo","vidu-q3-pro","vidu-q3-turbo",
|
|
143
|
+
"minimax-std","minimax-pro","minimax-2.3-pro","minimax-2.3-std",
|
|
144
|
+
"ltx-2","ltx-2-fast","ltx-2-19b","ltx-2.3",
|
|
145
|
+
"sora","sora-2","sora-2-pro","sora-2-standard","sora-2-storyboard",
|
|
146
|
+
"ovi","grok","happy-horse","happy-horse-720"]},
|
|
147
|
+
"duration": {"type": "integer", "description": "Duration in seconds", "default": 5},
|
|
148
|
+
"aspect_ratio": {"type": "string", "default": "16:9"},
|
|
149
|
+
},
|
|
150
|
+
"required": ["prompt"],
|
|
151
|
+
},
|
|
152
|
+
"outputSchema": _prediction_output_schema(),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "muapi_video_from_image",
|
|
156
|
+
"description": "Animate an image into a video using muapi.ai.",
|
|
157
|
+
"endpoint": None,
|
|
158
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
159
|
+
"inputSchema": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"properties": {
|
|
162
|
+
"prompt": {"type": "string", "description": "Motion/animation prompt"},
|
|
163
|
+
"image_url": {"type": "string", "description": "Source image URL", "format": "uri"},
|
|
164
|
+
"model": {"type": "string", "default": "kling-std",
|
|
165
|
+
"enum": ["veo3","veo3-fast","veo3.1","veo3.1-fast","veo3.1-ref",
|
|
166
|
+
"veo3.1-lite","veo4",
|
|
167
|
+
"kling-std","kling-pro","kling-master",
|
|
168
|
+
"kling-v2.5-pro","kling-v2.5-std","kling-v2.6-pro",
|
|
169
|
+
"kling-v3-pro","kling-v3-std","kling-v3-4k",
|
|
170
|
+
"kling-v3-omni","kling-v3-omni-std","kling-v3-omni-4k",
|
|
171
|
+
"kling-o1","kling-o1-std","kling-o1-ref",
|
|
172
|
+
"wan2.1","wan2.1-ref","wan2.2","wan2.2-spicy",
|
|
173
|
+
"wan2.5","wan2.5-fast","wan2.6","wan2.7","wan2.7-ref",
|
|
174
|
+
"seedance-pro","seedance-pro-fast","seedance-lite",
|
|
175
|
+
"seedance-lite-ref","seedance-v1.5","seedance-v1.5-fast",
|
|
176
|
+
"seedance-v2","seedance-v2-omni",
|
|
177
|
+
"seedance-2","seedance-2-fast","seedance-2-flf",
|
|
178
|
+
"seedance-2-omni","seedance-2-vip",
|
|
179
|
+
"hunyuan","runway","runway-act-two",
|
|
180
|
+
"pixverse-v4.5","pixverse-v5","pixverse-v5.5",
|
|
181
|
+
"pixverse-v6","pixverse-v6-trans",
|
|
182
|
+
"vidu","vidu-q1-ref","vidu-q2-pro","vidu-q2-turbo",
|
|
183
|
+
"vidu-q2-ref","vidu-q2-start-end",
|
|
184
|
+
"vidu-q3-pro","vidu-q3-turbo","vidu-q3-flf",
|
|
185
|
+
"midjourney",
|
|
186
|
+
"minimax-std","minimax-pro","minimax-2.3-pro",
|
|
187
|
+
"minimax-2.3-std","minimax-2.3-fast",
|
|
188
|
+
"ltx-2","ltx-2-fast","ltx-2-19b","ltx-2.3",
|
|
189
|
+
"sora-2","sora-2-pro","sora-2-standard",
|
|
190
|
+
"ovi","grok","leonardo",
|
|
191
|
+
"happy-horse","happy-horse-ref",
|
|
192
|
+
"infinitetalk","video-effects","wan-effects"]},
|
|
193
|
+
"duration": {"type": "integer", "default": 5},
|
|
194
|
+
"aspect_ratio": {"type": "string", "default": "16:9"},
|
|
195
|
+
},
|
|
196
|
+
"required": ["prompt", "image_url"],
|
|
197
|
+
},
|
|
198
|
+
"outputSchema": _prediction_output_schema(),
|
|
199
|
+
},
|
|
200
|
+
# ── Audio ────────────────────────────────────────────────────────────────
|
|
201
|
+
{
|
|
202
|
+
"name": "muapi_audio_create",
|
|
203
|
+
"description": "Create original music using Suno via muapi.ai.",
|
|
204
|
+
"endpoint": "suno-create-music",
|
|
205
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
206
|
+
"inputSchema": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"properties": {
|
|
209
|
+
"prompt": {"type": "string", "description": "Music description or lyrics"},
|
|
210
|
+
"title": {"type": "string", "default": ""},
|
|
211
|
+
"tags": {"type": "string", "description": "Genre/style tags", "default": ""},
|
|
212
|
+
"make_instrumental": {"type": "boolean", "default": False},
|
|
213
|
+
},
|
|
214
|
+
"required": ["prompt"],
|
|
215
|
+
},
|
|
216
|
+
"outputSchema": _prediction_output_schema(),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "muapi_audio_from_text",
|
|
220
|
+
"description": "Generate sound effects or ambient audio from a text prompt using MMAudio.",
|
|
221
|
+
"endpoint": "mmaudio-v2/text-to-audio",
|
|
222
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
223
|
+
"inputSchema": {
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": {
|
|
226
|
+
"prompt": {"type": "string"},
|
|
227
|
+
"duration": {"type": "number", "default": 10.0},
|
|
228
|
+
},
|
|
229
|
+
"required": ["prompt"],
|
|
230
|
+
},
|
|
231
|
+
"outputSchema": _prediction_output_schema(),
|
|
232
|
+
},
|
|
233
|
+
# ── Enhance ──────────────────────────────────────────────────────────────
|
|
234
|
+
{
|
|
235
|
+
"name": "muapi_enhance_upscale",
|
|
236
|
+
"description": "Upscale an image using AI.",
|
|
237
|
+
"endpoint": "ai-image-upscale",
|
|
238
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": True},
|
|
239
|
+
"inputSchema": {
|
|
240
|
+
"type": "object",
|
|
241
|
+
"properties": {"image_url": {"type": "string", "format": "uri"}},
|
|
242
|
+
"required": ["image_url"],
|
|
243
|
+
},
|
|
244
|
+
"outputSchema": _prediction_output_schema(),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "muapi_enhance_bg_remove",
|
|
248
|
+
"description": "Remove the background from an image.",
|
|
249
|
+
"endpoint": "ai-background-remover",
|
|
250
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": True},
|
|
251
|
+
"inputSchema": {
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {"image_url": {"type": "string", "format": "uri"}},
|
|
254
|
+
"required": ["image_url"],
|
|
255
|
+
},
|
|
256
|
+
"outputSchema": _prediction_output_schema(),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"name": "muapi_enhance_face_swap",
|
|
260
|
+
"description": "Swap faces in an image or video.",
|
|
261
|
+
"endpoint": None,
|
|
262
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
263
|
+
"inputSchema": {
|
|
264
|
+
"type": "object",
|
|
265
|
+
"properties": {
|
|
266
|
+
"source_url": {"type": "string", "description": "Face source image URL", "format": "uri"},
|
|
267
|
+
"target_url": {"type": "string", "description": "Target image or video URL", "format": "uri"},
|
|
268
|
+
"mode": {"type": "string", "enum": ["image", "video"], "default": "image"},
|
|
269
|
+
},
|
|
270
|
+
"required": ["source_url", "target_url"],
|
|
271
|
+
},
|
|
272
|
+
"outputSchema": _prediction_output_schema(),
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "muapi_enhance_ghibli",
|
|
276
|
+
"description": "Convert an image to Studio Ghibli anime style.",
|
|
277
|
+
"endpoint": "ai-ghibli-style",
|
|
278
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": True},
|
|
279
|
+
"inputSchema": {
|
|
280
|
+
"type": "object",
|
|
281
|
+
"properties": {"image_url": {"type": "string", "format": "uri"}},
|
|
282
|
+
"required": ["image_url"],
|
|
283
|
+
},
|
|
284
|
+
"outputSchema": _prediction_output_schema(),
|
|
285
|
+
},
|
|
286
|
+
# ── Edit ─────────────────────────────────────────────────────────────────
|
|
287
|
+
{
|
|
288
|
+
"name": "muapi_edit_lipsync",
|
|
289
|
+
"description": "Sync lip movements in a video to an audio file.",
|
|
290
|
+
"endpoint": None,
|
|
291
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
292
|
+
"inputSchema": {
|
|
293
|
+
"type": "object",
|
|
294
|
+
"properties": {
|
|
295
|
+
"video_url": {"type": "string", "format": "uri"},
|
|
296
|
+
"audio_url": {"type": "string", "format": "uri"},
|
|
297
|
+
"model": {"type": "string", "default": "sync",
|
|
298
|
+
"enum": ["sync","latentsync","creatify","veed",
|
|
299
|
+
"ltx-2","ltx-2.3","kling-v1","kling-v2","wan2.2"]},
|
|
300
|
+
},
|
|
301
|
+
"required": ["video_url", "audio_url"],
|
|
302
|
+
},
|
|
303
|
+
"outputSchema": _prediction_output_schema(),
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "muapi_edit_clipping",
|
|
307
|
+
"description": "Extract AI-selected highlight clips from a long video.",
|
|
308
|
+
"endpoint": "ai-clipping",
|
|
309
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
310
|
+
"inputSchema": {
|
|
311
|
+
"type": "object",
|
|
312
|
+
"properties": {
|
|
313
|
+
"video_url": {"type": "string", "format": "uri"},
|
|
314
|
+
"num_highlights": {"type": "integer", "default": 3},
|
|
315
|
+
"aspect_ratio": {"type": "string", "default": "9:16"},
|
|
316
|
+
},
|
|
317
|
+
"required": ["video_url"],
|
|
318
|
+
},
|
|
319
|
+
"outputSchema": _prediction_output_schema(),
|
|
320
|
+
},
|
|
321
|
+
# ── Predict ──────────────────────────────────────────────────────────────
|
|
322
|
+
{
|
|
323
|
+
"name": "muapi_predict_result",
|
|
324
|
+
"description": "Fetch the current result of an async prediction by request ID.",
|
|
325
|
+
"endpoint": None,
|
|
326
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
327
|
+
"inputSchema": {
|
|
328
|
+
"type": "object",
|
|
329
|
+
"properties": {
|
|
330
|
+
"request_id": {"type": "string", "description": "Prediction request ID"},
|
|
331
|
+
},
|
|
332
|
+
"required": ["request_id"],
|
|
333
|
+
},
|
|
334
|
+
"outputSchema": _prediction_output_schema(),
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"name": "muapi_upload_file",
|
|
338
|
+
"description": "Upload a local file to muapi.ai and get back a hosted URL.",
|
|
339
|
+
"endpoint": None,
|
|
340
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
341
|
+
"inputSchema": {
|
|
342
|
+
"type": "object",
|
|
343
|
+
"properties": {
|
|
344
|
+
"file_path": {"type": "string", "description": "Absolute local file path"},
|
|
345
|
+
},
|
|
346
|
+
"required": ["file_path"],
|
|
347
|
+
},
|
|
348
|
+
"outputSchema": {
|
|
349
|
+
"type": "object",
|
|
350
|
+
"properties": {
|
|
351
|
+
"url": {"type": "string", "description": "Hosted file URL"},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
# ── Keys ─────────────────────────────────────────────────────────────────
|
|
356
|
+
{
|
|
357
|
+
"name": "muapi_keys_list",
|
|
358
|
+
"description": "List all API keys on the authenticated muapi.ai account.",
|
|
359
|
+
"endpoint": None,
|
|
360
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
361
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
362
|
+
"outputSchema": {
|
|
363
|
+
"type": "array",
|
|
364
|
+
"items": {
|
|
365
|
+
"type": "object",
|
|
366
|
+
"properties": {
|
|
367
|
+
"id": {"type": "integer"},
|
|
368
|
+
"name": {"type": "string"},
|
|
369
|
+
"is_active": {"type": "boolean"},
|
|
370
|
+
"created_at": {"type": "string"},
|
|
371
|
+
"last_used_at": {"type": "string"},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"name": "muapi_keys_create",
|
|
378
|
+
"description": "Create a new API key for the authenticated account. The raw key is returned once — store it immediately.",
|
|
379
|
+
"endpoint": None,
|
|
380
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
381
|
+
"inputSchema": {
|
|
382
|
+
"type": "object",
|
|
383
|
+
"properties": {
|
|
384
|
+
"name": {"type": "string", "description": "Label for the key", "default": "cli"},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
"outputSchema": {
|
|
388
|
+
"type": "object",
|
|
389
|
+
"properties": {
|
|
390
|
+
"id": {"type": "integer"},
|
|
391
|
+
"name": {"type": "string"},
|
|
392
|
+
"api_key": {"type": "string", "description": "Raw API key — shown once"},
|
|
393
|
+
},
|
|
394
|
+
"required": ["api_key"],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"name": "muapi_keys_delete",
|
|
399
|
+
"description": "Delete an API key by ID.",
|
|
400
|
+
"endpoint": None,
|
|
401
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
402
|
+
"inputSchema": {
|
|
403
|
+
"type": "object",
|
|
404
|
+
"properties": {
|
|
405
|
+
"key_id": {"type": "integer", "description": "Key ID from muapi_keys_list"},
|
|
406
|
+
},
|
|
407
|
+
"required": ["key_id"],
|
|
408
|
+
},
|
|
409
|
+
"outputSchema": {
|
|
410
|
+
"type": "object",
|
|
411
|
+
"properties": {"message": {"type": "string"}},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
# ── Workflow ──────────────────────────────────────────────────────────────
|
|
415
|
+
{
|
|
416
|
+
"name": "muapi_workflow_list",
|
|
417
|
+
"description": "List all saved workflows for the authenticated user.",
|
|
418
|
+
"endpoint": None,
|
|
419
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
420
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
421
|
+
"outputSchema": {"type": "array"},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"name": "muapi_workflow_create",
|
|
425
|
+
"description": "Generate a new multi-step AI workflow from a text description using the AI architect. Returns the workflow definition with nodes and connections.",
|
|
426
|
+
"endpoint": None,
|
|
427
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
428
|
+
"inputSchema": {
|
|
429
|
+
"type": "object",
|
|
430
|
+
"properties": {
|
|
431
|
+
"prompt": {"type": "string", "description": "Describe the workflow, e.g. 'generate image with flux then upscale it'"},
|
|
432
|
+
"sync": {"type": "boolean", "default": True},
|
|
433
|
+
},
|
|
434
|
+
"required": ["prompt"],
|
|
435
|
+
},
|
|
436
|
+
"outputSchema": {"type": "object"},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"name": "muapi_workflow_get",
|
|
440
|
+
"description": "Get a workflow definition by ID including its nodes and connections.",
|
|
441
|
+
"endpoint": None,
|
|
442
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
443
|
+
"inputSchema": {
|
|
444
|
+
"type": "object",
|
|
445
|
+
"properties": {"workflow_id": {"type": "string"}},
|
|
446
|
+
"required": ["workflow_id"],
|
|
447
|
+
},
|
|
448
|
+
"outputSchema": {"type": "object"},
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"name": "muapi_workflow_execute",
|
|
452
|
+
"description": "Execute a workflow with specific node inputs. Returns a run_id to poll with muapi_workflow_status.",
|
|
453
|
+
"endpoint": None,
|
|
454
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
455
|
+
"inputSchema": {
|
|
456
|
+
"type": "object",
|
|
457
|
+
"properties": {
|
|
458
|
+
"workflow_id": {"type": "string"},
|
|
459
|
+
"inputs": {"type": "object", "description": "Map of {node_id: {param: value}}"},
|
|
460
|
+
},
|
|
461
|
+
"required": ["workflow_id"],
|
|
462
|
+
},
|
|
463
|
+
"outputSchema": {"type": "object", "properties": {"run_id": {"type": "string"}}},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"name": "muapi_workflow_status",
|
|
467
|
+
"description": "Get the node-by-node status of a workflow run.",
|
|
468
|
+
"endpoint": None,
|
|
469
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
470
|
+
"inputSchema": {
|
|
471
|
+
"type": "object",
|
|
472
|
+
"properties": {"run_id": {"type": "string"}},
|
|
473
|
+
"required": ["run_id"],
|
|
474
|
+
},
|
|
475
|
+
"outputSchema": {"type": "object"},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
"name": "muapi_workflow_outputs",
|
|
479
|
+
"description": "Get the final output URLs of a completed workflow run.",
|
|
480
|
+
"endpoint": None,
|
|
481
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
482
|
+
"inputSchema": {
|
|
483
|
+
"type": "object",
|
|
484
|
+
"properties": {"run_id": {"type": "string"}},
|
|
485
|
+
"required": ["run_id"],
|
|
486
|
+
},
|
|
487
|
+
"outputSchema": {"type": "object"},
|
|
488
|
+
},
|
|
489
|
+
# ── Account ──────────────────────────────────────────────────────────────
|
|
490
|
+
{
|
|
491
|
+
"name": "muapi_account_balance",
|
|
492
|
+
"description": "Get the current account balance for the authenticated muapi.ai user.",
|
|
493
|
+
"endpoint": None,
|
|
494
|
+
"annotations": {"readOnlyHint": True, "idempotentHint": True},
|
|
495
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
496
|
+
"outputSchema": {
|
|
497
|
+
"type": "object",
|
|
498
|
+
"properties": {
|
|
499
|
+
"balance": {"type": "number", "description": "Current balance in USD"},
|
|
500
|
+
"currency": {"type": "string"},
|
|
501
|
+
"email": {"type": "string"},
|
|
502
|
+
},
|
|
503
|
+
"required": ["balance", "currency"],
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"name": "muapi_account_topup",
|
|
508
|
+
"description": "Create a Stripe checkout session to add credits to the muapi.ai account. Returns a checkout URL — open it in a browser to complete payment.",
|
|
509
|
+
"endpoint": None,
|
|
510
|
+
"annotations": {"readOnlyHint": False, "idempotentHint": False},
|
|
511
|
+
"inputSchema": {
|
|
512
|
+
"type": "object",
|
|
513
|
+
"properties": {
|
|
514
|
+
"amount": {"type": "integer", "description": "Amount in USD to add (minimum 1)", "default": 10, "minimum": 1},
|
|
515
|
+
"currency": {"type": "string", "default": "usd"},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
"outputSchema": {
|
|
519
|
+
"type": "object",
|
|
520
|
+
"properties": {
|
|
521
|
+
"checkout_url": {"type": "string", "format": "uri"},
|
|
522
|
+
"amount": {"type": "integer"},
|
|
523
|
+
"currency": {"type": "string"},
|
|
524
|
+
},
|
|
525
|
+
"required": ["checkout_url"],
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
]
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ── Tool dispatch ─────────────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
def _dispatch(tool_name: str, args: dict) -> dict:
|
|
534
|
+
"""Call the appropriate muapi endpoint for a tool name."""
|
|
535
|
+
from .image import T2I_MODELS, I2I_MODELS, WIDTH_HEIGHT_MODELS, LIST_INPUT_MODELS
|
|
536
|
+
from .video import T2V_MODELS, I2V_MODELS, LIST_INPUT_I2V
|
|
537
|
+
from .audio import app as _ # import to ensure module loaded
|
|
538
|
+
from .enhance import app as _
|
|
539
|
+
from .edit import app as _
|
|
540
|
+
|
|
541
|
+
LIPSYNC_MAP = {
|
|
542
|
+
"sync": "sync-lipsync",
|
|
543
|
+
"latentsync": "latentsync-video",
|
|
544
|
+
"creatify": "creatify-lipsync",
|
|
545
|
+
"veed": "veed-lipsync",
|
|
546
|
+
"ltx-2": "ltx-2-19b-lipsync",
|
|
547
|
+
"ltx-2.3": "ltx-2.3-lipsync",
|
|
548
|
+
"kling-v1": "kling-v1-avatar-pro",
|
|
549
|
+
"kling-v2": "kling-v2-avatar-pro",
|
|
550
|
+
"wan2.2": "wan2.2-speech-to-video",
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if tool_name == "muapi_image_generate":
|
|
554
|
+
model = args.get("model", "flux-dev")
|
|
555
|
+
endpoint = T2I_MODELS.get(model)
|
|
556
|
+
if not endpoint:
|
|
557
|
+
raise ValueError(f"Unknown image model: {model}")
|
|
558
|
+
payload = {"prompt": args["prompt"], "num_images": args.get("num_images", 1)}
|
|
559
|
+
if model in WIDTH_HEIGHT_MODELS:
|
|
560
|
+
payload["width"] = args.get("width", 1024)
|
|
561
|
+
payload["height"] = args.get("height", 1024)
|
|
562
|
+
else:
|
|
563
|
+
payload["aspect_ratio"] = args.get("aspect_ratio", "1:1")
|
|
564
|
+
return api_client.generate(endpoint, payload)
|
|
565
|
+
|
|
566
|
+
if tool_name == "muapi_image_edit":
|
|
567
|
+
model = args.get("model", "flux-kontext-dev")
|
|
568
|
+
endpoint = I2I_MODELS.get(model)
|
|
569
|
+
if not endpoint:
|
|
570
|
+
raise ValueError(f"Unknown image edit model: {model}")
|
|
571
|
+
payload = {
|
|
572
|
+
"prompt": args["prompt"],
|
|
573
|
+
"aspect_ratio": args.get("aspect_ratio", "1:1"),
|
|
574
|
+
"num_images": args.get("num_images", 1),
|
|
575
|
+
}
|
|
576
|
+
if model in LIST_INPUT_MODELS:
|
|
577
|
+
payload["images_list"] = [args["image_url"]]
|
|
578
|
+
else:
|
|
579
|
+
payload["image_url"] = args["image_url"]
|
|
580
|
+
return api_client.generate(endpoint, payload)
|
|
581
|
+
|
|
582
|
+
if tool_name == "muapi_video_generate":
|
|
583
|
+
model = args.get("model", "kling-master")
|
|
584
|
+
endpoint = T2V_MODELS.get(model)
|
|
585
|
+
if not endpoint:
|
|
586
|
+
raise ValueError(f"Unknown video model: {model}")
|
|
587
|
+
return api_client.generate(endpoint, {
|
|
588
|
+
"prompt": args["prompt"],
|
|
589
|
+
"duration": args.get("duration", 5),
|
|
590
|
+
"aspect_ratio": args.get("aspect_ratio", "16:9"),
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
if tool_name == "muapi_video_from_image":
|
|
594
|
+
model = args.get("model", "kling-std")
|
|
595
|
+
endpoint = I2V_MODELS.get(model)
|
|
596
|
+
if not endpoint:
|
|
597
|
+
raise ValueError(f"Unknown i2v model: {model}")
|
|
598
|
+
payload = {
|
|
599
|
+
"prompt": args["prompt"],
|
|
600
|
+
"duration": args.get("duration", 5),
|
|
601
|
+
"aspect_ratio": args.get("aspect_ratio", "16:9"),
|
|
602
|
+
}
|
|
603
|
+
if model in LIST_INPUT_I2V:
|
|
604
|
+
payload["images_list"] = [args["image_url"]]
|
|
605
|
+
else:
|
|
606
|
+
payload["image_url"] = args["image_url"]
|
|
607
|
+
return api_client.generate(endpoint, payload)
|
|
608
|
+
|
|
609
|
+
if tool_name == "muapi_audio_create":
|
|
610
|
+
return api_client.generate("suno-create-music", {
|
|
611
|
+
"prompt": args["prompt"], "title": args.get("title", ""),
|
|
612
|
+
"tags": args.get("tags", ""), "make_instrumental": args.get("make_instrumental", False),
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
if tool_name == "muapi_audio_from_text":
|
|
616
|
+
return api_client.generate("mmaudio-v2/text-to-audio", {
|
|
617
|
+
"prompt": args["prompt"], "duration": args.get("duration", 10.0),
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
if tool_name == "muapi_enhance_upscale":
|
|
621
|
+
return api_client.generate("ai-image-upscale", {"image_url": args["image_url"]})
|
|
622
|
+
|
|
623
|
+
if tool_name == "muapi_enhance_bg_remove":
|
|
624
|
+
return api_client.generate("ai-background-remover", {"image_url": args["image_url"]})
|
|
625
|
+
|
|
626
|
+
if tool_name == "muapi_enhance_face_swap":
|
|
627
|
+
ep = "ai-video-face-swap" if args.get("mode") == "video" else "ai-image-face-swap"
|
|
628
|
+
return api_client.generate(ep, {"source_url": args["source_url"], "target_url": args["target_url"]})
|
|
629
|
+
|
|
630
|
+
if tool_name == "muapi_enhance_ghibli":
|
|
631
|
+
return api_client.generate("ai-ghibli-style", {"image_url": args["image_url"]})
|
|
632
|
+
|
|
633
|
+
if tool_name == "muapi_edit_lipsync":
|
|
634
|
+
model = args.get("model", "sync")
|
|
635
|
+
ep = LIPSYNC_MAP.get(model, "lipsync")
|
|
636
|
+
return api_client.generate(ep, {"video_url": args["video_url"], "audio_url": args["audio_url"]})
|
|
637
|
+
|
|
638
|
+
if tool_name == "muapi_edit_clipping":
|
|
639
|
+
return api_client.generate("ai-clipping", {
|
|
640
|
+
"video_url": args["video_url"],
|
|
641
|
+
"num_highlights": args.get("num_highlights", 3),
|
|
642
|
+
"aspect_ratio": args.get("aspect_ratio", "9:16"),
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
if tool_name == "muapi_predict_result":
|
|
646
|
+
return api_client.get_result(args["request_id"])
|
|
647
|
+
|
|
648
|
+
if tool_name == "muapi_upload_file":
|
|
649
|
+
return api_client.upload_file(args["file_path"])
|
|
650
|
+
|
|
651
|
+
if tool_name == "muapi_workflow_list":
|
|
652
|
+
from ..config import BASE_URL, get_api_key
|
|
653
|
+
import httpx as _httpx
|
|
654
|
+
key = get_api_key()
|
|
655
|
+
if not key:
|
|
656
|
+
raise ValueError("No API key configured.")
|
|
657
|
+
wf_base = BASE_URL.replace("/api/v1", "") + "/workflow"
|
|
658
|
+
resp = _httpx.get(f"{wf_base}/get-workflow-defs", headers={"x-api-key": key}, timeout=30.0)
|
|
659
|
+
if resp.status_code >= 400:
|
|
660
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
661
|
+
return resp.json()
|
|
662
|
+
|
|
663
|
+
if tool_name == "muapi_workflow_create":
|
|
664
|
+
from ..config import BASE_URL, get_api_key
|
|
665
|
+
import httpx as _httpx
|
|
666
|
+
key = get_api_key()
|
|
667
|
+
if not key:
|
|
668
|
+
raise ValueError("No API key configured.")
|
|
669
|
+
wf_base = BASE_URL.replace("/api/v1", "") + "/workflow"
|
|
670
|
+
body = {"prompt": args["prompt"], "sync": args.get("sync", True)}
|
|
671
|
+
resp = _httpx.post(f"{wf_base}/architect", json=body, headers={"x-api-key": key}, timeout=120.0)
|
|
672
|
+
if resp.status_code >= 400:
|
|
673
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
674
|
+
return resp.json()
|
|
675
|
+
|
|
676
|
+
if tool_name == "muapi_workflow_get":
|
|
677
|
+
from ..config import BASE_URL, get_api_key
|
|
678
|
+
import httpx as _httpx
|
|
679
|
+
key = get_api_key()
|
|
680
|
+
if not key:
|
|
681
|
+
raise ValueError("No API key configured.")
|
|
682
|
+
wf_base = BASE_URL.replace("/api/v1", "") + "/workflow"
|
|
683
|
+
resp = _httpx.get(f"{wf_base}/get-workflow-def/{args['workflow_id']}", headers={"x-api-key": key}, timeout=30.0)
|
|
684
|
+
if resp.status_code >= 400:
|
|
685
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
686
|
+
return resp.json()
|
|
687
|
+
|
|
688
|
+
if tool_name == "muapi_workflow_execute":
|
|
689
|
+
from ..config import BASE_URL, get_api_key
|
|
690
|
+
import httpx as _httpx
|
|
691
|
+
key = get_api_key()
|
|
692
|
+
if not key:
|
|
693
|
+
raise ValueError("No API key configured.")
|
|
694
|
+
wf_base = BASE_URL.replace("/api/v1", "") + "/workflow"
|
|
695
|
+
body = {"inputs": args.get("inputs", {})}
|
|
696
|
+
resp = _httpx.post(f"{wf_base}/{args['workflow_id']}/api-execute", json=body,
|
|
697
|
+
headers={"x-api-key": key}, timeout=60.0)
|
|
698
|
+
if resp.status_code >= 400:
|
|
699
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
700
|
+
return resp.json()
|
|
701
|
+
|
|
702
|
+
if tool_name == "muapi_workflow_status":
|
|
703
|
+
from ..config import BASE_URL, get_api_key
|
|
704
|
+
import httpx as _httpx
|
|
705
|
+
key = get_api_key()
|
|
706
|
+
if not key:
|
|
707
|
+
raise ValueError("No API key configured.")
|
|
708
|
+
wf_base = BASE_URL.replace("/api/v1", "") + "/workflow"
|
|
709
|
+
resp = _httpx.get(f"{wf_base}/run/{args['run_id']}/status", headers={"x-api-key": key}, timeout=30.0)
|
|
710
|
+
if resp.status_code >= 400:
|
|
711
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
712
|
+
return resp.json()
|
|
713
|
+
|
|
714
|
+
if tool_name == "muapi_workflow_outputs":
|
|
715
|
+
from ..config import BASE_URL, get_api_key
|
|
716
|
+
import httpx as _httpx
|
|
717
|
+
key = get_api_key()
|
|
718
|
+
if not key:
|
|
719
|
+
raise ValueError("No API key configured.")
|
|
720
|
+
wf_base = BASE_URL.replace("/api/v1", "") + "/workflow"
|
|
721
|
+
resp = _httpx.get(f"{wf_base}/run/{args['run_id']}/api-outputs", headers={"x-api-key": key}, timeout=30.0)
|
|
722
|
+
if resp.status_code >= 400:
|
|
723
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
724
|
+
return resp.json()
|
|
725
|
+
|
|
726
|
+
if tool_name == "muapi_keys_list":
|
|
727
|
+
from ..config import BASE_URL, get_api_key
|
|
728
|
+
import httpx as _httpx
|
|
729
|
+
key = get_api_key()
|
|
730
|
+
if not key:
|
|
731
|
+
raise ValueError("No API key configured. Run: muapi auth configure")
|
|
732
|
+
resp = _httpx.get(f"{BASE_URL}/keys", headers={"x-api-key": key}, timeout=30.0)
|
|
733
|
+
if resp.status_code >= 400:
|
|
734
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
735
|
+
return resp.json()
|
|
736
|
+
|
|
737
|
+
if tool_name == "muapi_keys_create":
|
|
738
|
+
from ..config import BASE_URL, get_api_key
|
|
739
|
+
import httpx as _httpx
|
|
740
|
+
key = get_api_key()
|
|
741
|
+
if not key:
|
|
742
|
+
raise ValueError("No API key configured. Run: muapi auth configure")
|
|
743
|
+
resp = _httpx.post(
|
|
744
|
+
f"{BASE_URL}/keys",
|
|
745
|
+
json={"name": args.get("name", "cli")},
|
|
746
|
+
headers={"x-api-key": key},
|
|
747
|
+
timeout=30.0,
|
|
748
|
+
)
|
|
749
|
+
if resp.status_code >= 400:
|
|
750
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
751
|
+
return resp.json()
|
|
752
|
+
|
|
753
|
+
if tool_name == "muapi_keys_delete":
|
|
754
|
+
from ..config import BASE_URL, get_api_key
|
|
755
|
+
import httpx as _httpx
|
|
756
|
+
key = get_api_key()
|
|
757
|
+
if not key:
|
|
758
|
+
raise ValueError("No API key configured. Run: muapi auth configure")
|
|
759
|
+
resp = _httpx.delete(
|
|
760
|
+
f"{BASE_URL}/keys/{args['key_id']}",
|
|
761
|
+
headers={"x-api-key": key},
|
|
762
|
+
timeout=30.0,
|
|
763
|
+
)
|
|
764
|
+
if resp.status_code >= 400:
|
|
765
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
766
|
+
return resp.json()
|
|
767
|
+
|
|
768
|
+
if tool_name == "muapi_account_balance":
|
|
769
|
+
from ..config import BASE_URL, get_api_key
|
|
770
|
+
import httpx as _httpx
|
|
771
|
+
key = get_api_key()
|
|
772
|
+
if not key:
|
|
773
|
+
raise ValueError("No API key configured. Run: muapi auth configure")
|
|
774
|
+
resp = _httpx.get(f"{BASE_URL}/account/balance", headers={"x-api-key": key}, timeout=30.0)
|
|
775
|
+
if resp.status_code >= 400:
|
|
776
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
777
|
+
return resp.json()
|
|
778
|
+
|
|
779
|
+
if tool_name == "muapi_account_topup":
|
|
780
|
+
from ..config import BASE_URL, get_api_key
|
|
781
|
+
import httpx as _httpx
|
|
782
|
+
key = get_api_key()
|
|
783
|
+
if not key:
|
|
784
|
+
raise ValueError("No API key configured. Run: muapi auth configure")
|
|
785
|
+
payload = {"amount": args.get("amount", 10), "currency": args.get("currency", "usd")}
|
|
786
|
+
resp = _httpx.post(f"{BASE_URL}/account/topup", json=payload, headers={"x-api-key": key}, timeout=30.0)
|
|
787
|
+
if resp.status_code >= 400:
|
|
788
|
+
raise api_client.MuapiError(resp.text, resp.status_code)
|
|
789
|
+
return resp.json()
|
|
790
|
+
|
|
791
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ── MCP stdio server ──────────────────────────────────────────────────────────
|
|
795
|
+
|
|
796
|
+
def _mcp_response(id: Any, result: Any) -> str:
|
|
797
|
+
return json.dumps({"jsonrpc": "2.0", "id": id, "result": result})
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _mcp_error(id: Any, code: int, message: str) -> str:
|
|
801
|
+
return json.dumps({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}})
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _tool_result(data: Any, is_error: bool = False) -> dict:
|
|
805
|
+
text = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
|
|
806
|
+
result = {
|
|
807
|
+
"content": [{"type": "text", "text": text}],
|
|
808
|
+
"isError": is_error,
|
|
809
|
+
}
|
|
810
|
+
if not is_error and isinstance(data, dict):
|
|
811
|
+
result["structuredContent"] = data
|
|
812
|
+
return result
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _handle_request(request: dict) -> str:
|
|
816
|
+
method = request.get("method", "")
|
|
817
|
+
req_id = request.get("id")
|
|
818
|
+
params = request.get("params", {})
|
|
819
|
+
|
|
820
|
+
if method == "initialize":
|
|
821
|
+
return _mcp_response(req_id, {
|
|
822
|
+
"protocolVersion": "2025-06-18",
|
|
823
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
824
|
+
"serverInfo": {"name": "muapi", "version": "0.1.0"},
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
if method == "tools/list":
|
|
828
|
+
tools_list = []
|
|
829
|
+
for t in TOOLS:
|
|
830
|
+
entry = {
|
|
831
|
+
"name": t["name"],
|
|
832
|
+
"description": t["description"],
|
|
833
|
+
"inputSchema": t["inputSchema"],
|
|
834
|
+
"annotations": t.get("annotations", {}),
|
|
835
|
+
}
|
|
836
|
+
if "outputSchema" in t:
|
|
837
|
+
entry["outputSchema"] = t["outputSchema"]
|
|
838
|
+
tools_list.append(entry)
|
|
839
|
+
return _mcp_response(req_id, {"tools": tools_list})
|
|
840
|
+
|
|
841
|
+
if method == "tools/call":
|
|
842
|
+
tool_name = params.get("name", "")
|
|
843
|
+
arguments = params.get("arguments", {})
|
|
844
|
+
try:
|
|
845
|
+
result = _dispatch(tool_name, arguments)
|
|
846
|
+
return _mcp_response(req_id, _tool_result(result, is_error=False))
|
|
847
|
+
except api_client.MuapiError as e:
|
|
848
|
+
return _mcp_response(req_id, _tool_result({"error": str(e)}, is_error=True))
|
|
849
|
+
except ValueError as e:
|
|
850
|
+
return _mcp_error(req_id, -32602, str(e))
|
|
851
|
+
except Exception as e:
|
|
852
|
+
return _mcp_response(req_id, _tool_result({"error": str(e)}, is_error=True))
|
|
853
|
+
|
|
854
|
+
if method == "notifications/initialized":
|
|
855
|
+
return "" # No response for notifications
|
|
856
|
+
|
|
857
|
+
# Unknown method
|
|
858
|
+
return _mcp_error(req_id, -32601, f"Method not found: {method}")
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
@app.command("serve")
|
|
862
|
+
def serve(
|
|
863
|
+
check_auth: bool = typer.Option(True, "--check-auth/--no-check-auth",
|
|
864
|
+
help="Verify API key is configured before starting"),
|
|
865
|
+
):
|
|
866
|
+
"""Start the muapi MCP server (stdio transport).
|
|
867
|
+
|
|
868
|
+
Add to Claude Desktop config:
|
|
869
|
+
|
|
870
|
+
\\b
|
|
871
|
+
{
|
|
872
|
+
"mcpServers": {
|
|
873
|
+
"muapi": {
|
|
874
|
+
"command": "muapi",
|
|
875
|
+
"args": ["mcp", "serve"],
|
|
876
|
+
"env": { "MUAPI_API_KEY": "your-key-here" }
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
"""
|
|
881
|
+
if check_auth and not get_api_key():
|
|
882
|
+
sys.stderr.write(
|
|
883
|
+
json.dumps({"error": "No MUAPI_API_KEY configured. Set env var or run: muapi auth configure"}) + "\n"
|
|
884
|
+
)
|
|
885
|
+
sys.exit(3)
|
|
886
|
+
|
|
887
|
+
sys.stderr.write(json.dumps({"status": "muapi MCP server ready", "tools": len(TOOLS), "version": "0.1.0"}) + "\n")
|
|
888
|
+
sys.stderr.flush()
|
|
889
|
+
|
|
890
|
+
for line in sys.stdin:
|
|
891
|
+
line = line.strip()
|
|
892
|
+
if not line:
|
|
893
|
+
continue
|
|
894
|
+
try:
|
|
895
|
+
request = json.loads(line)
|
|
896
|
+
except json.JSONDecodeError:
|
|
897
|
+
response = _mcp_error(None, -32700, "Parse error")
|
|
898
|
+
sys.stdout.write(response + "\n")
|
|
899
|
+
sys.stdout.flush()
|
|
900
|
+
continue
|
|
901
|
+
|
|
902
|
+
response = _handle_request(request)
|
|
903
|
+
if response:
|
|
904
|
+
sys.stdout.write(response + "\n")
|
|
905
|
+
sys.stdout.flush()
|