media-agent-mcp 2.6.13__py3-none-any.whl → 2.7.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.
- media_agent_mcp/__init__.py +2 -2
- media_agent_mcp/ai_models/omni_human.py +2 -1
- media_agent_mcp/async_server.py +2 -324
- {media_agent_mcp-2.6.13.dist-info → media_agent_mcp-2.7.0.dist-info}/METADATA +1 -1
- {media_agent_mcp-2.6.13.dist-info → media_agent_mcp-2.7.0.dist-info}/RECORD +8 -8
- {media_agent_mcp-2.6.13.dist-info → media_agent_mcp-2.7.0.dist-info}/WHEEL +0 -0
- {media_agent_mcp-2.6.13.dist-info → media_agent_mcp-2.7.0.dist-info}/entry_points.txt +0 -0
- {media_agent_mcp-2.6.13.dist-info → media_agent_mcp-2.7.0.dist-info}/top_level.txt +0 -0
media_agent_mcp/__init__.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
"""Media Agent MCP Server - A Model Context Protocol server for media processing."""
|
2
2
|
|
3
3
|
from . import ai_models, media_selectors, storage, video
|
4
|
-
from .async_server import main
|
4
|
+
from .async_server import main as async_main
|
5
5
|
from . import async_wrapper
|
6
6
|
|
7
7
|
__version__ = "0.1.0"
|
8
|
-
__all__ = ['ai_models', 'media_selectors', 'storage', 'video', 'main', 'async_wrapper']
|
8
|
+
__all__ = ['ai_models', 'media_selectors', 'storage', 'video', 'main', 'async_main', 'async_wrapper']
|
media_agent_mcp/async_server.py
CHANGED
@@ -26,12 +26,6 @@ from dotenv import load_dotenv
|
|
26
26
|
import uvicorn
|
27
27
|
import anyio
|
28
28
|
from functools import wraps
|
29
|
-
import uuid
|
30
|
-
import weakref
|
31
|
-
from starlette.applications import Starlette
|
32
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
33
|
-
from starlette.requests import Request
|
34
|
-
from starlette.responses import Response
|
35
29
|
|
36
30
|
def async_retry(max_retries=3, delay=2):
|
37
31
|
def decorator(func):
|
@@ -50,20 +44,6 @@ def async_retry(max_retries=3, delay=2):
|
|
50
44
|
await asyncio.sleep(delay)
|
51
45
|
continue
|
52
46
|
return result
|
53
|
-
except anyio.ClosedResourceError as e:
|
54
|
-
logger.warning(f"ClosedResourceError in {func.__name__} (attempt {attempt + 1}): {e}")
|
55
|
-
# For ClosedResourceError, we should handle it gracefully
|
56
|
-
if attempt < max_retries - 1:
|
57
|
-
logger.info(f"Retrying {func.__name__} after ClosedResourceError...")
|
58
|
-
await asyncio.sleep(delay)
|
59
|
-
continue
|
60
|
-
else:
|
61
|
-
# On final attempt, return a structured error response
|
62
|
-
return {
|
63
|
-
"status": "error",
|
64
|
-
"data": None,
|
65
|
-
"message": f"Session expired during {func.__name__} execution. Please retry with a new session."
|
66
|
-
}
|
67
47
|
except Exception as e:
|
68
48
|
last_exception = str(e)
|
69
49
|
logger.error(f"Attempt {attempt + 1} of {max_retries} failed for {func.__name__} with exception: {e}. Retrying in {delay}s...")
|
@@ -77,46 +57,6 @@ def async_retry(max_retries=3, delay=2):
|
|
77
57
|
return wrapper
|
78
58
|
return decorator
|
79
59
|
|
80
|
-
def session_aware_retry(max_retries=3, delay=2):
|
81
|
-
"""Enhanced retry decorator that handles session expiration specifically."""
|
82
|
-
def decorator(func):
|
83
|
-
@wraps(func)
|
84
|
-
async def wrapper(*args, **kwargs):
|
85
|
-
for attempt in range(max_retries):
|
86
|
-
try:
|
87
|
-
result = await func(*args, **kwargs)
|
88
|
-
return result
|
89
|
-
except anyio.ClosedResourceError as e:
|
90
|
-
logger.warning(f"Session expired during {func.__name__} execution (attempt {attempt + 1}): {e}")
|
91
|
-
|
92
|
-
if attempt < max_retries - 1:
|
93
|
-
# Generate new session for retry
|
94
|
-
new_session_id = session_manager.generate_session_id()
|
95
|
-
logger.info(f"Generated new session {new_session_id} for retry of {func.__name__}")
|
96
|
-
await asyncio.sleep(delay)
|
97
|
-
continue
|
98
|
-
else:
|
99
|
-
return {
|
100
|
-
"status": "error",
|
101
|
-
"data": None,
|
102
|
-
"message": f"Session expired during {func.__name__} execution. A new session has been generated. Please retry your request."
|
103
|
-
}
|
104
|
-
except Exception as e:
|
105
|
-
logger.error(f"Unexpected error in {func.__name__} (attempt {attempt + 1}): {e}")
|
106
|
-
if attempt < max_retries - 1:
|
107
|
-
await asyncio.sleep(delay)
|
108
|
-
continue
|
109
|
-
else:
|
110
|
-
return {
|
111
|
-
"status": "error",
|
112
|
-
"data": None,
|
113
|
-
"message": f"Function {func.__name__} failed: {str(e)}"
|
114
|
-
}
|
115
|
-
|
116
|
-
return {"status": "error", "data": None, "message": f"Function {func.__name__} failed after {max_retries} retries"}
|
117
|
-
return wrapper
|
118
|
-
return decorator
|
119
|
-
|
120
60
|
from mcp.server.fastmcp import FastMCP
|
121
61
|
|
122
62
|
# Import async wrappers
|
@@ -141,178 +81,6 @@ from media_agent_mcp.async_wrapper import (
|
|
141
81
|
logging.basicConfig(level=logging.INFO)
|
142
82
|
logger = logging.getLogger(__name__)
|
143
83
|
|
144
|
-
# Session management for handling expired sessions
|
145
|
-
class SessionManager:
|
146
|
-
def __init__(self):
|
147
|
-
self._sessions = {} # Changed from WeakValueDictionary to regular dict
|
148
|
-
self._session_routes = {}
|
149
|
-
self._session_timestamps = {} # Track session creation times
|
150
|
-
|
151
|
-
def generate_session_id(self) -> str:
|
152
|
-
"""Generate a new unique session ID."""
|
153
|
-
return str(uuid.uuid4()).replace('-', '')
|
154
|
-
|
155
|
-
def register_session(self, session_id: str, session_obj):
|
156
|
-
"""Register a session object."""
|
157
|
-
import time
|
158
|
-
self._sessions[session_id] = session_obj
|
159
|
-
self._session_timestamps[session_id] = time.time()
|
160
|
-
logger.info(f"Registered session: {session_id}")
|
161
|
-
|
162
|
-
def get_session(self, session_id: str):
|
163
|
-
"""Get session object by ID."""
|
164
|
-
return self._sessions.get(session_id)
|
165
|
-
|
166
|
-
def remove_session(self, session_id: str):
|
167
|
-
"""Remove a session."""
|
168
|
-
if session_id in self._sessions:
|
169
|
-
del self._sessions[session_id]
|
170
|
-
if session_id in self._session_routes:
|
171
|
-
del self._session_routes[session_id]
|
172
|
-
if session_id in self._session_timestamps:
|
173
|
-
del self._session_timestamps[session_id]
|
174
|
-
logger.info(f"Removed session: {session_id}")
|
175
|
-
|
176
|
-
def cleanup_expired_sessions(self, max_age_seconds: int = 3600):
|
177
|
-
"""Clean up sessions older than max_age_seconds."""
|
178
|
-
import time
|
179
|
-
current_time = time.time()
|
180
|
-
expired_sessions = []
|
181
|
-
|
182
|
-
for session_id, timestamp in self._session_timestamps.items():
|
183
|
-
if current_time - timestamp > max_age_seconds:
|
184
|
-
expired_sessions.append(session_id)
|
185
|
-
|
186
|
-
for session_id in expired_sessions:
|
187
|
-
self.remove_session(session_id)
|
188
|
-
logger.info(f"Cleaned up expired session: {session_id}")
|
189
|
-
|
190
|
-
return len(expired_sessions)
|
191
|
-
|
192
|
-
def get_session_count(self) -> int:
|
193
|
-
"""Get the number of active sessions."""
|
194
|
-
return len(self._sessions)
|
195
|
-
|
196
|
-
def get_route_count(self) -> int:
|
197
|
-
"""Get the number of route mappings."""
|
198
|
-
return len(self._session_routes)
|
199
|
-
|
200
|
-
def add_route_mapping(self, old_session_id: str, new_session_id: str):
|
201
|
-
"""Add route mapping for session forwarding."""
|
202
|
-
self._session_routes[old_session_id] = new_session_id
|
203
|
-
logger.info(f"Added route mapping: {old_session_id} -> {new_session_id}")
|
204
|
-
|
205
|
-
def get_route_mapping(self, session_id: str) -> Optional[str]:
|
206
|
-
"""Get route mapping for a session."""
|
207
|
-
return self._session_routes.get(session_id)
|
208
|
-
|
209
|
-
# Global session manager
|
210
|
-
session_manager = SessionManager()
|
211
|
-
|
212
|
-
class SessionErrorHandlingMiddleware(BaseHTTPMiddleware):
|
213
|
-
"""Middleware to handle ClosedResourceError and auto-regenerate sessions."""
|
214
|
-
|
215
|
-
async def dispatch(self, request: Request, call_next):
|
216
|
-
try:
|
217
|
-
response = await call_next(request)
|
218
|
-
return response
|
219
|
-
except anyio.ClosedResourceError as e:
|
220
|
-
logger.warning(f"ClosedResourceError detected: {e}")
|
221
|
-
|
222
|
-
# Extract session_id from request
|
223
|
-
session_id = self._extract_session_id(request)
|
224
|
-
if session_id:
|
225
|
-
# Generate new session ID
|
226
|
-
new_session_id = session_manager.generate_session_id()
|
227
|
-
|
228
|
-
# Add route mapping
|
229
|
-
session_manager.add_route_mapping(session_id, new_session_id)
|
230
|
-
|
231
|
-
# Remove old session
|
232
|
-
session_manager.remove_session(session_id)
|
233
|
-
|
234
|
-
logger.info(f"Auto-generated new session {new_session_id} to replace expired session {session_id}")
|
235
|
-
|
236
|
-
# Create a JSON response with new session information
|
237
|
-
new_url = str(request.url).replace(f"session_id={session_id}", f"session_id={new_session_id}")
|
238
|
-
|
239
|
-
from starlette.responses import JSONResponse
|
240
|
-
return JSONResponse(
|
241
|
-
status_code=410, # Gone - indicates the resource is no longer available
|
242
|
-
content={
|
243
|
-
"error": "session_expired",
|
244
|
-
"message": "Session expired, please use the new session ID",
|
245
|
-
"old_session_id": session_id,
|
246
|
-
"new_session_id": new_session_id,
|
247
|
-
"redirect_url": new_url
|
248
|
-
}
|
249
|
-
)
|
250
|
-
|
251
|
-
# If no session_id found, re-raise the error
|
252
|
-
raise e
|
253
|
-
except Exception as e:
|
254
|
-
logger.error(f"Unexpected error in middleware: {e}")
|
255
|
-
raise e
|
256
|
-
|
257
|
-
def _extract_session_id(self, request: Request) -> Optional[str]:
|
258
|
-
"""Extract session_id from request URL or headers."""
|
259
|
-
# Try to get from query parameters
|
260
|
-
session_id = request.query_params.get('session_id')
|
261
|
-
if session_id:
|
262
|
-
return session_id
|
263
|
-
|
264
|
-
# Try to get from path parameters
|
265
|
-
if hasattr(request, 'path_params') and 'session_id' in request.path_params:
|
266
|
-
return request.path_params['session_id']
|
267
|
-
|
268
|
-
# Try to extract from URL path
|
269
|
-
import re
|
270
|
-
path = str(request.url.path)
|
271
|
-
match = re.search(r'session_id=([a-f0-9]+)', str(request.url))
|
272
|
-
if match:
|
273
|
-
return match.group(1)
|
274
|
-
|
275
|
-
return None
|
276
|
-
|
277
|
-
# Global exception handler for ClosedResourceError
|
278
|
-
async def handle_closed_resource_error(request, exc):
|
279
|
-
"""Global handler for ClosedResourceError exceptions."""
|
280
|
-
logger.error(f"Global ClosedResourceError handler triggered: {exc}")
|
281
|
-
|
282
|
-
# Extract session_id from request if possible
|
283
|
-
session_id = None
|
284
|
-
if hasattr(request, 'query_params'):
|
285
|
-
session_id = request.query_params.get('session_id')
|
286
|
-
|
287
|
-
if session_id:
|
288
|
-
# Generate new session ID
|
289
|
-
new_session_id = session_manager.generate_session_id()
|
290
|
-
session_manager.add_route_mapping(session_id, new_session_id)
|
291
|
-
session_manager.remove_session(session_id)
|
292
|
-
|
293
|
-
logger.info(f"Global handler: Generated new session {new_session_id} to replace {session_id}")
|
294
|
-
|
295
|
-
from starlette.responses import JSONResponse
|
296
|
-
return JSONResponse(
|
297
|
-
status_code=410, # Gone - indicates the resource is no longer available
|
298
|
-
content={
|
299
|
-
"error": "session_expired",
|
300
|
-
"message": "Session has expired. A new session has been generated.",
|
301
|
-
"old_session_id": session_id,
|
302
|
-
"new_session_id": new_session_id,
|
303
|
-
"action": "retry_with_new_session"
|
304
|
-
}
|
305
|
-
)
|
306
|
-
|
307
|
-
from starlette.responses import JSONResponse
|
308
|
-
return JSONResponse(
|
309
|
-
status_code=500,
|
310
|
-
content={
|
311
|
-
"error": "internal_server_error",
|
312
|
-
"message": "An internal server error occurred. Please try again."
|
313
|
-
}
|
314
|
-
)
|
315
|
-
|
316
84
|
# Initialize FastMCP server (will be configured in main function)
|
317
85
|
load_dotenv()
|
318
86
|
mcp = FastMCP("Media-Agent-MCP-Async")
|
@@ -668,76 +436,6 @@ async def tts_tool(text: str, speaker_id: str) -> dict:
|
|
668
436
|
return result
|
669
437
|
|
670
438
|
|
671
|
-
@mcp.tool()
|
672
|
-
async def get_session_status() -> dict:
|
673
|
-
"""
|
674
|
-
Get current session management status and statistics.
|
675
|
-
|
676
|
-
Returns:
|
677
|
-
Dictionary with session statistics and status information
|
678
|
-
"""
|
679
|
-
try:
|
680
|
-
active_sessions = session_manager.get_session_count()
|
681
|
-
route_mappings = session_manager.get_route_count()
|
682
|
-
|
683
|
-
# Clean up expired sessions (older than 1 hour)
|
684
|
-
cleaned_sessions = session_manager.cleanup_expired_sessions(3600)
|
685
|
-
|
686
|
-
return {
|
687
|
-
"status": "success",
|
688
|
-
"data": {
|
689
|
-
"active_sessions": active_sessions,
|
690
|
-
"route_mappings": route_mappings,
|
691
|
-
"cleaned_sessions": cleaned_sessions,
|
692
|
-
"session_manager_enabled": True,
|
693
|
-
"features": [
|
694
|
-
"automatic_session_recovery",
|
695
|
-
"closed_resource_error_handling",
|
696
|
-
"request_forwarding",
|
697
|
-
"session_route_mapping",
|
698
|
-
"automatic_session_cleanup"
|
699
|
-
]
|
700
|
-
},
|
701
|
-
"message": f"Session management is active. Cleaned {cleaned_sessions} expired sessions."
|
702
|
-
}
|
703
|
-
except Exception as e:
|
704
|
-
logger.error(f"Error getting session status: {e}")
|
705
|
-
return {
|
706
|
-
"status": "error",
|
707
|
-
"data": None,
|
708
|
-
"message": f"Failed to get session status: {str(e)}"
|
709
|
-
}
|
710
|
-
|
711
|
-
|
712
|
-
@mcp.tool()
|
713
|
-
async def generate_new_session() -> dict:
|
714
|
-
"""
|
715
|
-
Manually generate a new session ID for testing or recovery purposes.
|
716
|
-
|
717
|
-
Returns:
|
718
|
-
Dictionary with new session ID
|
719
|
-
"""
|
720
|
-
try:
|
721
|
-
new_session_id = session_manager.generate_session_id()
|
722
|
-
logger.info(f"Manually generated new session: {new_session_id}")
|
723
|
-
|
724
|
-
return {
|
725
|
-
"status": "success",
|
726
|
-
"data": {
|
727
|
-
"session_id": new_session_id,
|
728
|
-
"timestamp": asyncio.get_event_loop().time()
|
729
|
-
},
|
730
|
-
"message": f"New session generated: {new_session_id}"
|
731
|
-
}
|
732
|
-
except Exception as e:
|
733
|
-
logger.error(f"Error generating new session: {e}")
|
734
|
-
return {
|
735
|
-
"status": "error",
|
736
|
-
"data": None,
|
737
|
-
"message": f"Failed to generate new session: {str(e)}"
|
738
|
-
}
|
739
|
-
|
740
|
-
|
741
439
|
def main():
|
742
440
|
"""Main entry point for the Async MCP server."""
|
743
441
|
import os
|
@@ -799,11 +497,6 @@ def main():
|
|
799
497
|
logger.info(f"Transport: {args.transport}")
|
800
498
|
if args.transport == 'sse':
|
801
499
|
logger.info(f"SSE Server will run on {args.host}:{args.port}")
|
802
|
-
logger.info("Session management features enabled:")
|
803
|
-
logger.info(" - Automatic session expiration detection")
|
804
|
-
logger.info(" - Auto-generation of new session IDs")
|
805
|
-
logger.info(" - Request forwarding with route mapping")
|
806
|
-
logger.info(" - ClosedResourceError handling")
|
807
500
|
|
808
501
|
logger.info("Available async tools:")
|
809
502
|
logger.info(" 1. video_last_frame_tool_async - Extract last frame from video and upload to TOS")
|
@@ -821,11 +514,8 @@ def main():
|
|
821
514
|
logger.info(" 13. install_tools_plugin_async - Install development tools (ffmpeg and ffprobe)")
|
822
515
|
logger.info(" 14. omni_human_tool_async - Generate a video using Omni Human AI model")
|
823
516
|
logger.info(" 15. google_edit_tool_async - Edit images with Google Gemini (async)")
|
824
|
-
logger.info(" 16. get_session_status - Get current session management status and statistics")
|
825
|
-
logger.info(" 17. generate_new_session - Manually generate a new session ID")
|
826
517
|
logger.info("")
|
827
518
|
logger.info("All tools support concurrent execution using asyncio.gather() or run_multiple_tools_concurrently()")
|
828
|
-
logger.info("Session management tools (16-17) help monitor and manage connection sessions")
|
829
519
|
|
830
520
|
try:
|
831
521
|
# Start the server with specified transport
|
@@ -833,24 +523,12 @@ def main():
|
|
833
523
|
logger.info(f"Starting async SSE server on {args.host}:{args.port}")
|
834
524
|
mcp.settings.host = args.host
|
835
525
|
mcp.settings.port = args.port
|
836
|
-
|
837
|
-
# Get the SSE app and add session error handling middleware
|
838
|
-
sse_app = mcp.sse_app()
|
839
|
-
|
840
|
-
# Add session error handling middleware
|
841
|
-
sse_app.add_middleware(SessionErrorHandlingMiddleware)
|
842
|
-
|
843
|
-
# Add global exception handler for ClosedResourceError
|
844
|
-
sse_app.add_exception_handler(anyio.ClosedResourceError, handle_closed_resource_error)
|
845
|
-
|
846
|
-
logger.info("Added SessionErrorHandlingMiddleware and global ClosedResourceError handler for automatic session recovery")
|
847
|
-
|
848
526
|
# Use uvicorn to run SSE app with extended keep-alive timeout (5 minutes)
|
849
527
|
uvicorn.run(
|
850
|
-
sse_app,
|
528
|
+
mcp.sse_app(),
|
851
529
|
host=args.host,
|
852
530
|
port=args.port,
|
853
|
-
timeout_keep_alive=
|
531
|
+
timeout_keep_alive=1200
|
854
532
|
)
|
855
533
|
else:
|
856
534
|
# Default stdio transport
|
@@ -1,8 +1,8 @@
|
|
1
|
-
media_agent_mcp/__init__.py,sha256=
|
2
|
-
media_agent_mcp/async_server.py,sha256=
|
1
|
+
media_agent_mcp/__init__.py,sha256=s1rx7OkUul6PrxbxziYAV6c0AuamPlnelltQieHqX_I,340
|
2
|
+
media_agent_mcp/async_server.py,sha256=boXtiFLw2mnr6ZWNZTRhchd4KAZXhbGRnOFqX-ZaJNk,19872
|
3
3
|
media_agent_mcp/async_wrapper.py,sha256=hiiBhhz9WeVDfSBWVh6ovhf5jeP5ZbsieBbz9P-KPn0,15351
|
4
4
|
media_agent_mcp/ai_models/__init__.py,sha256=2kHzTYwjQw89U4QGDq0e2WqJScqDkDNlDaWHGak5JeY,553
|
5
|
-
media_agent_mcp/ai_models/omni_human.py,sha256=
|
5
|
+
media_agent_mcp/ai_models/omni_human.py,sha256=d6fll3qqj41Wmv6K0dd8iA_LL7kJWqHuEgxbmqf9JbY,4670
|
6
6
|
media_agent_mcp/ai_models/openaiedit.py,sha256=uu4d2BgXSrjWRdNPs_SryI9muxO93pItVtEze9nDhjc,9776
|
7
7
|
media_agent_mcp/ai_models/seed16.py,sha256=cX0ZONj2Jpu_dzSIq8oXSJfnsfGWVcaEmWyRxg6jMfQ,5110
|
8
8
|
media_agent_mcp/ai_models/seedance.py,sha256=ni7LtXn4jTn5wX2NtcWDMj5Eea8LoP1QLYgwSx_GvBs,9014
|
@@ -43,8 +43,8 @@ media_agent_mcp/video/__init__.py,sha256=tfz22XEeFSeuKa3AggYCE0vCDt4IwXRCKW6avof
|
|
43
43
|
media_agent_mcp/video/processor.py,sha256=twfqmN5DbVryjDawZUcqTUcnglcBJYpUbAnApqHgD0c,12787
|
44
44
|
media_agent_mcp/video/stack.py,sha256=pyoJiJ9NhU1tjy2l3kARI9sWFoC00Fj97psxYOBi2NU,1736
|
45
45
|
media_agent_mcp/video/subtitle.py,sha256=TlrWVhWJqYTUJpnVz7eccwMAn8ixfrRzRxS6ETMY-DM,16323
|
46
|
-
media_agent_mcp-2.
|
47
|
-
media_agent_mcp-2.
|
48
|
-
media_agent_mcp-2.
|
49
|
-
media_agent_mcp-2.
|
50
|
-
media_agent_mcp-2.
|
46
|
+
media_agent_mcp-2.7.0.dist-info/METADATA,sha256=LFjhceAxy7-5kSQoeuYu0MHb5aX_aA2yp1tJgegWjW4,11305
|
47
|
+
media_agent_mcp-2.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
48
|
+
media_agent_mcp-2.7.0.dist-info/entry_points.txt,sha256=qhOUwR-ORVf9GO7emhhl7Lgd6MISgqbZr8bEuSH_VdA,70
|
49
|
+
media_agent_mcp-2.7.0.dist-info/top_level.txt,sha256=WEa0YfchpTxZgiKn8gdxYgs-dir5HepJaTOrxAGx9nY,16
|
50
|
+
media_agent_mcp-2.7.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|