-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
390 lines (323 loc) · 15.8 KB
/
Copy pathserver.py
File metadata and controls
390 lines (323 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
import os
import httpx
import base64
import mimetypes
from datetime import datetime
from PIL import Image
from mcp.server.fastmcp import FastMCP
from prompts import SEARCH_PROMPT, IMAGE_ENHANCE_PROMPT, DOC_GEN_PROMPT
mcp = FastMCP(
"gptnix-toolkit",
instructions="Elite toolkit powered by Gemini Flash 3.1. Provides deep web research via looped grounding, file analysis, and image generation."
)
API_URL = "https://api.gptnix.online/v1/chat/completions"
SEARCH_MODEL = "google/gemini-3.1-flash-lite-preview"
IMAGE_MODEL = "google/gemini-3.1-flash-image-preview"
def get_headers():
key = os.getenv("AKASH_API_KEY")
if not key:
raise ValueError("AKASH_API_KEY not set")
return {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
async def call_api_loop(query: str, loops: int = 2, temperature: float = 0.2) -> str:
messages = [
{"role": "system", "content": SEARCH_PROMPT},
{"role": "user", "content": f"Initial query: {query}. Begin step 1 research."}
]
async with httpx.AsyncClient(timeout=120.0) as client:
payload = {"model": SEARCH_MODEL, "temperature": temperature, "messages": messages, "stream": True}
# Step 1: Initial Query
async with client.stream("POST", API_URL, headers=get_headers(), json=payload) as r:
r.raise_for_status()
current_response = ""
async for chunk in r.aiter_text():
if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n":
import json
try:
data = json.loads(chunk[6:])
if content := data.get("choices", [{}])[0].get("delta", {}).get("content"):
current_response += content
# yield current_response if we were implementing true MCP stream wrapper
except json.JSONDecodeError:
pass
messages.append({"role": "model", "content": current_response})
# Loop steps 2 to loops
for step in range(2, loops + 1):
messages.append({"role": "user", "content": f"Continue. This is step {step}. Find different/deeper aspects."})
payload["messages"] = messages
async with client.stream("POST", API_URL, headers=get_headers(), json=payload) as r:
r.raise_for_status()
current_response = ""
async for chunk in r.aiter_text():
if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n":
import json
try:
data = json.loads(chunk[6:])
if content := data.get("choices", [{}])[0].get("delta", {}).get("content"):
current_response += content
except json.JSONDecodeError:
pass
messages.append({"role": "model", "content": current_response})
# Final Step: Compile
messages.append({"role": "user", "content": "Final Answer. Compile everything into the required final format."})
payload["messages"] = messages
final_answer = ""
async with client.stream("POST", API_URL, headers=get_headers(), json=payload) as r:
r.raise_for_status()
async for chunk in r.aiter_text():
if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n":
import json
try:
data = json.loads(chunk[6:])
if content := data.get("choices", [{}])[0].get("delta", {}).get("content"):
final_answer += content
except json.JSONDecodeError:
pass
return final_answer
@mcp.tool()
async def WebSearch(query: str, loops: int = 2) -> str:
"""
Comprehensive, deep internet search using Looped Grounding.
Args:
query: The search query. Provide targeted, well-instructed queries. Do not use generic phrases like "search the whole internet".
loops: Number of search loops to perform (1 to 5). Defaults to 2.
"""
loops = max(1, min(5, loops))
return await call_api_loop(query, loops=loops)
@mcp.tool()
async def GenerateImage(prompts: list[str], output_paths: list[str] = None, input_images: list[str] = None) -> str:
"""
Generate one or multiple images using the Gemini Flash Image model.
To ensure high quality, provide EXTREMELY detailed, exact, and structurally perfect prompts. Do not use short or ambiguous terms.
Args:
prompts: A list of text descriptions. The model AUTO-UNDERSTANDS aspect ratios (e.g., 16:9, 1:1) and styles from the prompt text natively. Each prompt in the list will generate one independent image (max 5 allowed).
output_paths: Optional list of FULL ABSOLUTE file paths to save the images. Must match the length of prompts if provided. ALWAYS use ABSOLUTE paths.
input_images: Optional list of FULL ABSOLUTE image file paths to use as reference/input. Applied to all generated images in this batch.
"""
input_images = input_images or []
if isinstance(prompts, str):
prompts = [prompts]
if output_paths and isinstance(output_paths, str):
output_paths = [output_paths]
if output_paths and len(output_paths) != len(prompts):
return f"Error: Number of output_paths ({len(output_paths)}) does not match number of prompts ({len(prompts)})."
# Cap to max 5 concurrent requests
prompts = prompts[:5]
# Base payload construction for shared inputs
base_content = []
for img_path in input_images:
if not os.path.exists(img_path):
return f"Error: Input image file not found: {img_path}"
mime_type, _ = mimetypes.guess_type(img_path)
if not mime_type or not mime_type.startswith("image/"):
mime_type = "image/jpeg"
with open(img_path, "rb") as f:
b64_img = base64.b64encode(f.read()).decode("utf-8")
data_url = f"data:{mime_type};base64,{b64_img}"
base_content.append({
"type": "image_url",
"image_url": {"url": data_url}
})
saved_files = []
errors = []
import asyncio
async def enhance_prompt(client: httpx.AsyncClient, prompt_text: str) -> str:
payload = {
"model": SEARCH_MODEL,
"messages": [
{"role": "system", "content": IMAGE_ENHANCE_PROMPT},
{"role": "user", "content": f"Enhance this prompt:\n{prompt_text}"}
]
}
try:
r = await client.post(API_URL, headers=get_headers(), json=payload)
if r.status_code == 200:
enhanced = r.json().get("choices", [{}])[0].get("message", {}).get("content", "")
if enhanced:
return enhanced.strip()
except:
pass
# Fallback to original prompt if enhancement fails
return prompt_text
async def request_image(idx, prompt_text):
async with httpx.AsyncClient(timeout=120.0) as client:
# First, enhance the prompt
enhanced_prompt_text = await enhance_prompt(client, prompt_text)
content = [{"type": "text", "text": enhanced_prompt_text}] + base_content
payload = {
"model": IMAGE_MODEL,
"messages": [
{"role": "user", "content": content}
],
"modalities": ["image", "text"]
}
try:
r = await client.post(API_URL, headers=get_headers(), json=payload)
if r.status_code != 200:
return None, f"Prompt [{idx+1}] Failed API Error ({r.status_code}): {r.text} | Attempted Prompt: '{enhanced_prompt_text[:50]}...'"
data = r.json()
choices = data.get("choices", [])
if not choices:
return None, f"Prompt [{idx+1}] Error: No choices returned from the model."
message = choices[0].get("message", {})
images = message.get("images", [])
if not images:
return None, f"Prompt [{idx+1}] Error: No image found for prompt '{enhanced_prompt_text[:50]}...'"
b64_data_url = images[0].get("image_url", {}).get("url", "")
if not b64_data_url:
return None, f"Prompt [{idx+1}] Error: Image URL empty for prompt '{enhanced_prompt_text[:50]}...'"
b64_string = b64_data_url.split(",")[1] if "," in b64_data_url else b64_data_url
# Path resolution
if output_paths and len(output_paths) > idx:
target_path = os.path.abspath(output_paths[idx])
os.makedirs(os.path.dirname(target_path) or ".", exist_ok=True)
else:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
target_path = os.path.abspath(f"image_{idx+1}_{timestamp}.png")
with open(target_path, "wb") as f:
f.write(base64.b64decode(b64_string))
return target_path, None
except Exception as exc:
return None, f"Prompt [{idx+1}] Exception: {str(exc)}"
# Run requests concurrently
tasks = [request_image(idx, pt) for idx, pt in enumerate(prompts)]
results = await asyncio.gather(*tasks)
for path, err in results:
if path:
saved_files.append(path)
if err:
errors.append(err)
result_str = ""
if saved_files:
result_str += "Success! Images saved to:\n" + "\n".join(f"- {f}" for f in saved_files)
if errors:
result_str += "\n\nErrors encountered:\n" + "\n".join(f"- {e}" for e in errors)
if not saved_files:
return result_str
return result_str
@mcp.tool()
async def AnalyzeImage(image_path: str, query: str) -> str:
"""
Analyze local images (screenshots, UI mockups, diagrams) and answer questions or write code based on them.
Args:
image_path: FULL ABSOLUTE path to the image file to analyze.
query: What to do with the image (e.g., "Write React code to recreate this UI" or "What does this diagram show?").
"""
if not os.path.exists(image_path):
return f"Error: Image file not found: {image_path}"
mime_type, _ = mimetypes.guess_type(image_path)
if not mime_type or not mime_type.startswith("image/"):
# Attempt fallback
mime_type = "image/jpeg"
try:
with open(image_path, "rb") as f:
b64_img = base64.b64encode(f.read()).decode("utf-8")
except Exception as e:
return f"Error reading image: {e}"
data_url = f"data:{mime_type};base64,{b64_img}"
payload = {
"model": SEARCH_MODEL, # Flash Lite is fast and capable for multimodal tasks
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": query},
{"type": "image_url", "image_url": {"url": data_url}}
]
}
]
}
async with httpx.AsyncClient(timeout=120.0) as client:
r = await client.post(API_URL, headers=get_headers(), json=payload)
if r.status_code != 200:
return f"API Error ({r.status_code}): {r.text}"
data = r.json()
return data.get("choices", [{}])[0].get("message", {}).get("content", "Error: No content returned")
@mcp.tool()
async def ProcessImageFormat(input_path: str, output_path: str, width: int = None, height: int = None, quality: int = 90) -> str:
"""
Convert image formats, resize, or compress local images. Supported formats: PNG, JPEG, WEBP, ICO, GIF, BMP.
Args:
input_path: FULL ABSOLUTE path to the source image.
output_path: FULL ABSOLUTE path for the destination image. The format is determined by the file extension (e.g. .ico, .webp, .jpg).
width: Optional new width in pixels.
height: Optional new height in pixels.
quality: Optional quality for JPEG/WEBP (1-100, default 90).
"""
if not os.path.exists(input_path):
return f"Error: Input image not found: {input_path}"
try:
with Image.open(input_path) as img:
# Resize if requested
if width or height:
w = width or img.width
h = height or img.height
img = img.resize((w, h), Image.Resampling.LANCZOS)
# Convert mode if necessary for certain formats (e.g. JPEG doesn't support RGBA)
ext = os.path.splitext(output_path)[1].lower()
if ext in ['.jpg', '.jpeg'] and img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
elif ext == '.ico':
# ICOs require specific sizing, optionally handle it here
pass
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
# Save format specific options
save_kwargs = {}
if ext in ['.jpg', '.jpeg', '.webp']:
save_kwargs['quality'] = quality
img.save(output_path, **save_kwargs)
return f"Successfully processed and saved image to {os.path.abspath(output_path)} (Size: {img.width}x{img.height})"
except Exception as e:
return f"Error processing image: {e}"
@mcp.tool()
async def AutoDocGen(file_paths: list[str], instruction: str = "Generate comprehensive documentation (README format) for these files.") -> str:
"""
Generate professional Markdown documentation from massive source code contexts.
Uses Gemini 3.1 Flash Lite's 1 Million token window to analyze entire project structures at once.
Args:
file_paths: A list of FULL ABSOLUTE file paths to the source code files you want to document.
instruction: Specific instructions (e.g., 'Generate a README', 'Write inline docs for all auth functions'). Default is a general README format.
"""
if isinstance(file_paths, str):
file_paths = [file_paths]
MAX_FILES = 50 # Cap to prevent memory/payload explosion locally
if len(file_paths) > MAX_FILES:
file_paths = file_paths[:MAX_FILES]
documents = []
# Read all files locally
for path in file_paths:
if not os.path.exists(path) or not os.path.isfile(path):
continue
try:
# Simple text extraction, ignoring heavy binaries or giant minified blocks
with open(path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
# Skip files that look like huge minified binaries or are > 1MB
if len(content) > 1000000:
continue
documents.append(f"--- FILE: {os.path.basename(path)} ---\n{content}\n")
except Exception:
continue
if not documents:
return "Error: Could not read any of the provided files. Are they valid source code?"
combined_context = "\n".join(documents)
payload = {
"model": SEARCH_MODEL,
"messages": [
{"role": "system", "content": DOC_GEN_PROMPT},
{"role": "user", "content": f"Instructions:\n{instruction}\n\nContext Files:\n{combined_context}"}
]
}
async with httpx.AsyncClient(timeout=180.0) as client:
try:
r = await client.post(API_URL, headers=get_headers(), json=payload)
if r.status_code != 200:
return f"API Error ({r.status_code}): {r.text}"
data = r.json()
doc_content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
if not doc_content:
return "Error: Model returned empty documentation."
return doc_content.strip()
except Exception as e:
return f"Exception during documentation generation: {str(e)}"
if __name__ == "__main__":
mcp.run()