diff --git a/ai-hub/app/api/routes/mcp.py b/ai-hub/app/api/routes/mcp.py index 6f0f77c..e2d2019 100644 --- a/ai-hub/app/api/routes/mcp.py +++ b/ai-hub/app/api/routes/mcp.py @@ -225,8 +225,12 @@ method = body.get("method", "") params = body.get("params", {}) + from app.db.session import get_db_session + with get_db_session() as db: + user_id = await _get_authenticated_user(request, token, db) + logger.info(f"[MCP] [{session_id[:8]}] → {method}") - asyncio.create_task(_dispatch(queue, rpc_id, method, params, token, services)) + asyncio.create_task(_dispatch(queue, rpc_id, method, params, user_id, services)) return JSONResponse( {"status": "accepted"}, @@ -346,6 +350,142 @@ "session_id": {"type": "string", "description": "Optional session ID"}, }, required=["node_id", "path"]), + _tool_def("deploy_agent", + "Deploy a new agent instance with a template and session.", + { + "name": {"type": "string", "description": "Name of the agent"}, + "mesh_node_id": {"type": "string", "description": "Node ID to deploy to"}, + "description": {"type": "string", "description": "Optional description"}, + "system_prompt": {"type": "string", "description": "Optional system prompt override"}, + "max_loop_iterations": {"type": "integer", "description": "Max loop iterations (default 20)"}, + "initial_prompt": {"type": "string", "description": "Optional first message to kick off execution"}, + "provider_name": {"type": "string", "description": "Optional LLM provider name"}, + "trigger_type": {"type": "string", "description": "Trigger type: manual, webhook, cron, interval"}, + "cron_expression": {"type": "string", "description": "Cron expression if trigger_type is cron"}, + "interval_seconds": {"type": "integer", "description": "Interval in seconds if trigger_type is interval"}, + "default_prompt": {"type": "string", "description": "Predefined prompt for any trigger"}, + "co_worker_quality_gate": {"type": "boolean", "description": "Enable quality gate"}, + "rework_threshold": {"type": "integer", "description": "Rework threshold (0-100)"}, + "max_rework_attempts": {"type": "integer", "description": "Max rework attempts"} + }, + required=["name", "mesh_node_id"]), + _tool_def("update_agent_config", + "Update configuration for an existing agent.", + { + "agent_id": {"type": "string", "description": "Unique agent ID"}, + "name": {"type": "string", "description": "New name"}, + "system_prompt": {"type": "string", "description": "New system prompt"}, + "max_loop_iterations": {"type": "integer", "description": "New max loop iterations"}, + "mesh_node_id": {"type": "string", "description": "New mesh node ID"}, + "provider_name": {"type": "string", "description": "New provider name"}, + "model_name": {"type": "string", "description": "New model name"}, + "co_worker_quality_gate": {"type": "boolean", "description": "Enable quality gate"}, + "rework_threshold": {"type": "integer", "description": "New rework threshold"}, + "max_rework_attempts": {"type": "integer", "description": "New max rework attempts"} + }, + required=["agent_id", "mesh_node_id"]), + _tool_def("delete_agent", + "Delete an autonomous agent.", + { + "agent_id": {"type": "string", "description": "Unique agent ID to delete"} + }, + required=["agent_id"]), + _tool_def("get_agent_details", + "Get full details for a specific autonomous agent.", + { + "agent_id": {"type": "string", "description": "Unique agent ID"} + }, + required=["agent_id"]), + _tool_def("get_user_config", + "Get current user preferences and effective system configuration.", + {}), + _tool_def("update_user_config", + "Update user preferences for LLM, TTS, and STT.", + { + "llm": {"type": "object", "description": "LLM preferences"}, + "tts": {"type": "object", "description": "TTS preferences"}, + "stt": {"type": "object", "description": "STT preferences"}, + "statuses": {"type": "object", "description": "Provider health statuses"} + }), + _tool_def("list_groups", "List all security groups (Admin Only).", {}), + _tool_def("create_group", "Create a new security policy group (Admin Only).", { + "name": {"type": "string", "description": "Group name"}, + "description": {"type": "string", "description": "Optional description"}, + "policy": {"type": "object", "description": "Optional policy whitelists"} + }, required=["name"]), + _tool_def("update_group", "Update a security policy group (Admin Only).", { + "gid": {"type": "string", "description": "Group ID"}, + "name": {"type": "string", "description": "Group name"}, + "description": {"type": "string", "description": "Optional description"}, + "policy": {"type": "object", "description": "Optional policy whitelists"} + }, required=["gid"]), + _tool_def("delete_group", "Delete a security policy group (Admin Only).", { + "gid": {"type": "string", "description": "Group ID"} + }, required=["gid"]), + _tool_def("list_users", "List all registered users (Admin Only).", {}), + _tool_def("update_user_role", "Update a user's role (Admin Only).", { + "uid": {"type": "string", "description": "User ID"}, + "role": {"type": "string", "description": "New role (admin/user)"} + }, required=["uid", "role"]), + _tool_def("assign_user_to_group", "Assign a user to a policy group (Admin Only).", { + "uid": {"type": "string", "description": "User ID"}, + "group_id": {"type": "string", "description": "Group ID"} + }, required=["uid", "group_id"]), + _tool_def("get_user_profile", "Get the current user's profile information.", {}), + _tool_def("update_user_profile", "Update the current user's profile information.", { + "username": {"type": "string"}, + "full_name": {"type": "string"}, + "avatar_url": {"type": "string"} + }), + _tool_def("create_session", "Create a new chat session.", { + "provider_name": {"type": "string"}, + "model_name": {"type": "string"}, + "feature_name": {"type": "string"}, + "stt_provider_name": {"type": "string"}, + "tts_provider_name": {"type": "string"} + }), + _tool_def("get_session", "Get full detail for a specific session.", { + "session_id": {"type": "integer"} + }, required=["session_id"]), + _tool_def("list_sessions", "List all chat sessions for the current user.", { + "feature_name": {"type": "string", "description": "Optional feature filter"} + }), + _tool_def("update_session", "Update session configuration.", { + "session_id": {"type": "integer"}, + "title": {"type": "string"}, + "provider_name": {"type": "string"}, + "model_name": {"type": "string"}, + "restrict_skills": {"type": "boolean"}, + "allowed_skill_ids": {"type": "array", "items": {"type": "integer"}}, + "allowed_skill_names": {"type": "array", "items": {"type": "string"}}, + "system_prompt_override": {"type": "string"}, + "is_locked": {"type": "boolean"} + }, required=["session_id"]), + _tool_def("delete_session", "Archive a chat session.", { + "session_id": {"type": "integer"} + }, required=["session_id"]), + _tool_def("get_session_messages", "Retrieve the chat history for a session.", { + "session_id": {"type": "integer"} + }, required=["session_id"]), + _tool_def("clear_session_history", "Delete all messages in a session.", { + "session_id": {"type": "integer"} + }, required=["session_id"]), + _tool_def("attach_nodes_to_session", "Attach agent nodes to a session.", { + "session_id": {"type": "integer"}, + "node_ids": {"type": "array", "items": {"type": "string"}}, + "config": {"type": "object"} + }, required=["session_id", "node_ids"]), + _tool_def("detach_node_from_session", "Detach an agent node from a session.", { + "session_id": {"type": "integer"}, + "node_id": {"type": "string"} + }, required=["session_id", "node_id"]), + _tool_def("get_session_nodes", "Get all nodes attached to a session.", { + "session_id": {"type": "integer"} + }, required=["session_id"]), + _tool_def("cancel_session_task", "Cancel a running Swarm task within a session.", { + "session_id": {"type": "integer"} + }, required=["session_id"]), + _tool_def("get_system_status", "Retrieve full system and TLS state.", {}) ] } @@ -484,6 +624,110 @@ } return _ok(await loop.run_in_executor(None, _query)) + if name == "deploy_agent": + if not token: + raise ValueError("Authentication required to deploy agents.") + from app.api import schemas + def _execute_deploy(): + from app.db.session import get_db_session + with get_db_session() as db: + request = schemas.DeployAgentRequest(**args) + result = services.agent_service.deploy_agent(db, token, request) + return result + result = await loop.run_in_executor(None, _execute_deploy) + initial_prompt = args.get("initial_prompt") + if initial_prompt: + from app.core.orchestration.agent_loop import AgentExecutor + asyncio.create_task(AgentExecutor.run(result["instance_id"], initial_prompt, services, services.user_service)) + return _ok(result) + + if name == "update_agent_config": + if not token: + raise ValueError("Authentication required to update agent config.") + agent_id = args.get("agent_id") + if not agent_id: + raise ValueError("agent_id is required.") + from app.api import schemas + def _execute_update(): + from app.db.session import get_db_session + with get_db_session() as db: + config_args = {k: v for k, v in args.items() if k != "agent_id"} + request = schemas.AgentConfigUpdate(**config_args) + result = services.agent_service.update_config(db, agent_id, token, request) + return { + "id": str(result.id), + "status": result.status + } + return _ok(await loop.run_in_executor(None, _execute_update)) + + if name == "delete_agent": + if not token: + raise ValueError("Authentication required to delete agents.") + agent_id = args.get("agent_id") + if not agent_id: + raise ValueError("agent_id is required.") + def _execute_delete(): + from app.db.session import get_db_session + with get_db_session() as db: + services.agent_service.delete_agent(db, agent_id, token) + return {"status": "deleted", "agent_id": agent_id} + return _ok(await loop.run_in_executor(None, _execute_delete)) + + if name == "get_agent_details": + if not token: + raise ValueError("Authentication required to get agent details.") + agent_id = args.get("agent_id") + if not agent_id: + raise ValueError("agent_id is required.") + def _execute_query(): + from app.db.session import get_db_session + with get_db_session() as db: + a = services.agent_service.get_agent_instance(db, agent_id, token) + return { + "id": str(a.id), + "name": a.template.name if a.template else None, + "status": a.status, + "node": a.mesh_node_id, + "last_heartbeat": str(a.last_heartbeat) if a.last_heartbeat else None, + "total_runs": a.total_runs, + "successful_runs": a.successful_runs, + "quality_score": a.latest_quality_score, + "workspace_jail": a.current_workspace_jail, + "last_error": a.last_error, + "evaluation_status": a.evaluation_status, + } + return _ok(await loop.run_in_executor(None, _execute_query)) + + if name == "get_user_config": + if not token: + raise ValueError("Authentication required to get user config.") + def _execute_query(): + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + user = db.query(User).filter(User.id == token).first() + if not user: + raise ValueError("User not found.") + config = services.preference_service.merge_user_config(user, db) + return config.model_dump() + return _ok(await loop.run_in_executor(None, _execute_query)) + + if name == "update_user_config": + if not token: + raise ValueError("Authentication required to update user config.") + from app.api import schemas + def _execute_update(): + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + user = db.query(User).filter(User.id == token).first() + if not user: + raise ValueError("User not found.") + prefs = schemas.UserPreferences(**args) + result = services.preference_service.update_user_config(user, prefs, db) + return result.model_dump() + return _ok(await loop.run_in_executor(None, _execute_update)) + if name == "list_skills": if not token: raise ValueError("Authentication required to list skills.") @@ -567,10 +811,336 @@ return _ok(await loop.run_in_executor(None, _execute_delete)) + if name == "list_groups": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + groups = services.user_service.get_all_groups(db) + return [schemas.GroupInfo.model_validate(g).model_dump() for g in groups] + return _ok(await loop.run_in_executor(None, _query)) + + if name == "create_group": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + g = services.user_service.create_group(db, args.get("name"), args.get("description"), args.get("policy")) + if g is None: raise ValueError(f"Group '{args.get('name')}' already exists.") + return schemas.GroupInfo.model_validate(g).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "update_group": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + g = services.user_service.update_group(db, args.get("gid"), args.get("name"), args.get("description"), args.get("policy")) + if g is None: raise ValueError("Group not found.") + if g is False: raise ValueError(f"Group name conflict.") + return schemas.GroupInfo.model_validate(g).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "delete_group": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + success = services.user_service.delete_group(db, args.get("gid")) + if not success: raise ValueError("Failed to delete group.") + return {"message": "Group deleted successfully"} + return _ok(await loop.run_in_executor(None, _query)) + + if name == "list_users": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + users = services.user_service.get_all_users(db) + res = [] + for u_item in users: + p = schemas.UserProfile.model_validate(u_item) + if u_item.group: p.group_name = u_item.group.name + res.append(p.model_dump()) + return res + return _ok(await loop.run_in_executor(None, _query)) + + if name == "update_user_role": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + success = services.user_service.update_user_role(db, args.get("uid"), args.get("role")) + if not success: raise ValueError("Failed to update role.") + updated = services.user_service.get_user_by_id(db, args.get("uid")) + return schemas.UserProfile.model_validate(updated).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "assign_user_to_group": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = db.query(User).filter(User.id == token).first() + if not u or u.role != "admin": raise ValueError("Forbidden: Admin only.") + success = services.user_service.assign_user_to_group(db, args.get("uid"), args.get("group_id")) + if not success: raise ValueError("User or group not found.") + updated = services.user_service.get_user_by_id(db, args.get("uid")) + return schemas.UserProfile.model_validate(updated).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "get_user_profile": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = services.user_service.get_user_by_id(db=db, user_id=token) + if not u: raise ValueError("User not found.") + p = schemas.UserProfile.model_validate(u) + if u.group: p.group_name = u.group.name + return p.model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "update_user_profile": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import User + with get_db_session() as db: + u = services.user_service.get_user_by_id(db=db, user_id=token) + if not u: raise ValueError("User not found.") + if args.get("username"): u.username = args.get("username") + if args.get("full_name"): u.full_name = args.get("full_name") + if args.get("avatar_url"): u.avatar_url = args.get("avatar_url") + db.commit() + db.refresh(u) + p = schemas.UserProfile.model_validate(u) + if u.group: p.group_name = u.group.name + return p.model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "create_session": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + with get_db_session() as db: + s = services.session_service.create_session( + db=db, user_id=token, + provider_name=args.get("provider_name", "deepseek"), + model_name=args.get("model_name"), + feature_name=args.get("feature_name", "default"), + stt_provider_name=args.get("stt_provider_name"), + tts_provider_name=args.get("tts_provider_name") + ) + return schemas.Session.model_validate(s).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "get_session": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found.") + return schemas.Session.model_validate(s).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "list_sessions": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + sessions = db.query(DBSession).filter( + DBSession.user_id == token, + DBSession.feature_name == args.get("feature_name", "default"), + DBSession.is_archived == False + ).order_by(DBSession.created_at.desc()).all() + return [schemas.Session.model_validate(s).model_dump() for s in sessions] + return _ok(await loop.run_in_executor(None, _query)) + + if name == "update_session": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import Session as DBSession, Skill + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found.") + if args.get("title") is not None: s.title = args.get("title") + if args.get("provider_name") is not None: s.provider_name = args.get("provider_name") + if args.get("model_name") is not None: s.model_name = args.get("model_name") + if args.get("restrict_skills") is not None: s.restrict_skills = args.get("restrict_skills") + if args.get("allowed_skill_names") is not None: s.allowed_skill_names = args.get("allowed_skill_names") + if args.get("allowed_skill_ids") is not None: + skills = db.query(Skill).filter(Skill.id.in_(args.get("allowed_skill_ids"))).all() + s.skills = skills + if args.get("system_prompt_override") is not None: s.system_prompt_override = args.get("system_prompt_override") + if args.get("is_locked") is not None: s.is_locked = args.get("is_locked") + db.commit() + db.refresh(s) + return schemas.Session.model_validate(s).model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "delete_session": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found or access denied.") + services.session_service.archive_session(db, args.get("session_id")) + return {"message": "Session deleted successfully."} + return _ok(await loop.run_in_executor(None, _query)) + + if name == "get_session_messages": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found or access denied.") + messages = services.rag_service.get_message_history(db=db, session_id=args.get("session_id")) + res = [] + for m in messages: + msg_dict = schemas.Message.model_validate(m).model_dump() + if m.audio_path and os.path.exists(m.audio_path): + msg_dict["has_audio"] = True + msg_dict["audio_url"] = f"/sessions/messages/{m.id}/audio" + res.append(msg_dict) + return res + return _ok(await loop.run_in_executor(None, _query)) + + if name == "clear_session_history": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.db.session import get_db_session + from app.db.models import Session as DBSession, Message as DBMessage + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found.") + if s.is_locked: raise ValueError("Session is locked.") + deleted = db.query(DBMessage).filter(DBMessage.session_id == args.get("session_id")).delete() + db.commit() + return {"message": f"Cleared {deleted} messages."} + return _ok(await loop.run_in_executor(None, _query)) + + if name == "attach_nodes_to_session": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found or access denied.") + req = schemas.NodeAttachRequest(**args) + res = services.session_service.attach_nodes(db, args.get("session_id"), req) + return res.model_dump() + return _ok(await loop.run_in_executor(None, _query)) + + if name == "detach_node_from_session": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found.") + nodes = list(s.attached_node_ids or []) + if args.get("node_id") not in nodes: raise ValueError("Node not attached.") + nodes.remove(args.get("node_id")) + s.attached_node_ids = nodes + status = dict(s.node_sync_status or {}) + status.pop(args.get("node_id"), None) + s.node_sync_status = status + db.commit() + if hasattr(services, "orchestrator") and services.orchestrator: + services.orchestrator.assistant.clear_workspace(args.get("node_id"), s.sync_workspace_id) + return {"message": "Detached successfully."} + return _ok(await loop.run_in_executor(None, _query)) + + if name == "get_session_nodes": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.api import schemas + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found.") + sync_status = s.node_sync_status or {} + entries = [] + for nid in (s.attached_node_ids or []): + live = services.node_registry_service.get_node(nid) + persisted = sync_status.get(nid, {}) + status_val = "connected" if live and persisted.get("status") == "pending" else persisted.get("status", "pending") + entries.append(schemas.NodeSyncStatusEntry(node_id=nid, status=status_val, last_sync=persisted.get("last_sync"), error=persisted.get("error")).model_dump()) + return {"session_id": s.id, "sync_workspace_id": s.sync_workspace_id, "nodes": entries, "sync_config": s.sync_config or {}} + return _ok(await loop.run_in_executor(None, _query)) + + if name == "cancel_session_task": + if not token: raise ValueError("Authentication required.") + def _query(): + from app.db.session import get_db_session + from app.db.models import Session as DBSession + with get_db_session() as db: + s = db.query(DBSession).filter(DBSession.id == args.get("session_id"), DBSession.user_id == token).first() + if not s: raise ValueError("Session not found.") + s.is_cancelled = True + db.commit() + return {"message": "Cancellation request sent."} + return _ok(await loop.run_in_executor(None, _query)) + + if name == "get_system_status": + def _query(): + return { + "status": "running", + "oidc_enabled": settings.OIDC_ENABLED, + "tls_enabled": settings.GRPC_TLS_ENABLED, + "external_endpoint": settings.GRPC_EXTERNAL_ENDPOINT, + "version": settings.VERSION + } + return _ok(await loop.run_in_executor(None, _query)) + # Writable tools (future-proofing check) - # If OIDC is disabled, we block any tool that could manipulate the swarm mesh - # as plain Identity Claims are not secure enough for headless write operations. - writable_tools = [] # Planned tools + writable_tools = [] if name in writable_tools and not settings.OIDC_ENABLED: raise HTTPException( status_code=403, diff --git a/ai-hub/integration_tests/test_mcp_groups_sessions.py b/ai-hub/integration_tests/test_mcp_groups_sessions.py new file mode 100644 index 0000000..7b7d522 --- /dev/null +++ b/ai-hub/integration_tests/test_mcp_groups_sessions.py @@ -0,0 +1,155 @@ +import os +import httpx +import pytest +import conftest + +BASE_URL = os.getenv("SYNC_TEST_BASE_URL", "http://127.0.0.1:8002/api/v1") +def _headers(): + uid = os.getenv("SYNC_TEST_USER_ID", "") + return {"X-User-ID": uid, "Authorization": f"Bearer {uid}"} + +def test_mcp_group_lifecycle(): + with httpx.Client(timeout=10.0) as client: + # 1. Create Group via MCP + rpc_create = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "create_group", + "arguments": { + "name": "MCP Test Group", + "description": "Group created via MCP", + "policy": {"llm": ["gemini"]} + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_create, headers=_headers()) + print("DEBUG CREATE GROUP RESPONSE:", r.json()) + assert r.status_code == 200, r.text + res = r.json()["result"]["content"][0]["text"] + import json + g_data = json.loads(res) + assert "id" in g_data + assert g_data["name"] == "MCP Test Group" + gid = g_data["id"] + + # 2. List Groups via MCP + rpc_list = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "list_groups", + "arguments": {} + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_list, headers=_headers()) + assert r.status_code == 200 + res_list = json.loads(r.json()["result"]["content"][0]["text"]) + assert any(g["id"] == gid for g in res_list) + + # 3. Update Group via MCP + rpc_update = { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "update_group", + "arguments": { + "gid": gid, + "description": "Updated via MCP" + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_update, headers=_headers()) + assert r.status_code == 200 + res_up = json.loads(r.json()["result"]["content"][0]["text"]) + assert res_up["description"] == "Updated via MCP" + + # 4. Delete Group via MCP + rpc_del = { + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "delete_group", + "arguments": { + "gid": gid + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_del, headers=_headers()) + assert r.status_code == 200 + +def test_mcp_session_lifecycle(): + with httpx.Client(timeout=10.0) as client: + # 1. Create Session via MCP + rpc_create = { + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "create_session", + "arguments": { + "provider_name": "deepseek", + "feature_name": "mcp_test" + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_create, headers=_headers()) + assert r.status_code == 200, r.text + import json + s_data = json.loads(r.json()["result"]["content"][0]["text"]) + assert "id" in s_data + sid = s_data["id"] + + # 2. Get Session via MCP + rpc_get = { + "jsonrpc": "2.0", + "id": 6, + "method": "tools/call", + "params": { + "name": "get_session", + "arguments": { + "session_id": sid + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_get, headers=_headers()) + assert r.status_code == 200 + s_get = json.loads(r.json()["result"]["content"][0]["text"]) + assert s_get["id"] == sid + + # 3. List Sessions via MCP + rpc_list = { + "jsonrpc": "2.0", + "id": 7, + "method": "tools/call", + "params": { + "name": "list_sessions", + "arguments": { + "feature_name": "mcp_test" + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_list, headers=_headers()) + assert r.status_code == 200 + s_list = json.loads(r.json()["result"]["content"][0]["text"]) + assert len(s_list) > 0 + assert any(s["id"] == sid for s in s_list) + + # 4. Delete Session via MCP + rpc_del = { + "jsonrpc": "2.0", + "id": 8, + "method": "tools/call", + "params": { + "name": "delete_session", + "arguments": { + "session_id": sid + } + } + } + r = client.post(f"{BASE_URL}/mcp/", json=rpc_del, headers=_headers()) + assert r.status_code == 200