Coverage for integrations / social / api_mcp.py: 27.2%

81 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2HevolveSocial - MCP Tool Registry Blueprint 

34 endpoints for MCP server/tool discovery and registration. 

4Wires to frontend mcpApi in socialApi.js. 

5""" 

6import logging 

7from flask import Blueprint, request, jsonify, g 

8 

9from .auth import require_auth, optional_auth 

10from .models import get_db, MCPServer, MCPTool 

11 

12logger = logging.getLogger('hevolve_social') 

13 

14mcp_bp = Blueprint('mcp', __name__, url_prefix='/api/social') 

15 

16 

17def _ok(data=None, meta=None, status=200): 

18 r = {'success': True} 

19 if data is not None: 

20 r['data'] = data 

21 if meta is not None: 

22 r['meta'] = meta 

23 return jsonify(r), status 

24 

25 

26def _err(msg, status=400): 

27 return jsonify({'success': False, 'error': msg}), status 

28 

29 

30def _paginate(total, limit, offset): 

31 return {'total': total, 'limit': limit, 'offset': offset, 

32 'has_more': offset + limit < total} 

33 

34 

35def _get_json(): 

36 return request.get_json(force=True, silent=True) or {} 

37 

38 

39# ═══════════════════════════════════════════════════════════════ 

40# MCP TOOL REGISTRY (4 endpoints) 

41# ═══════════════════════════════════════════════════════════════ 

42 

43@mcp_bp.route('/mcp/servers', methods=['GET']) 

44@optional_auth 

45def list_mcp_servers(): 

46 """List registered MCP tool servers.""" 

47 db = get_db() 

48 try: 

49 limit = min(int(request.args.get('limit', 20)), 50) 

50 offset = int(request.args.get('offset', 0)) 

51 category = request.args.get('category') 

52 q = request.args.get('q', '').strip() 

53 

54 query = db.query(MCPServer).filter_by(is_active=True) 

55 if category: 

56 query = query.filter_by(category=category) 

57 if q: 

58 query = query.filter( 

59 MCPServer.name.ilike(f'%{q}%') | 

60 MCPServer.description.ilike(f'%{q}%') 

61 ) 

62 total = query.count() 

63 servers = query.order_by(MCPServer.created_at.desc()).offset(offset).limit(limit).all() 

64 return _ok([s.to_dict() for s in servers], _paginate(total, limit, offset)) 

65 except Exception as e: 

66 logger.error(f"mcp/servers GET error: {e}") 

67 return _err(str(e), 500) 

68 finally: 

69 db.close() 

70 

71 

72@mcp_bp.route('/mcp/servers/<server_id>/tools', methods=['GET']) 

73@optional_auth 

74def list_mcp_tools(server_id): 

75 """List tools for a specific MCP server.""" 

76 db = get_db() 

77 try: 

78 server = db.query(MCPServer).filter_by(id=server_id, is_active=True).first() 

79 if not server: 

80 return _err('Server not found', 404) 

81 tools = db.query(MCPTool).filter_by(server_id=server_id).all() 

82 return _ok({ 

83 'server': server.to_dict(), 

84 'tools': [t.to_dict() for t in tools], 

85 }) 

86 except Exception as e: 

87 logger.error(f"mcp/servers/{server_id}/tools error: {e}") 

88 return _err(str(e), 500) 

89 finally: 

90 db.close() 

91 

92 

93@mcp_bp.route('/mcp/register', methods=['POST']) 

94@require_auth 

95def register_mcp_server(): 

96 """Register a new MCP tool server with its tools.""" 

97 db = get_db() 

98 try: 

99 data = _get_json() 

100 name = data.get('name', '').strip() 

101 if not name: 

102 return _err('name required') 

103 

104 server = MCPServer( 

105 owner_id=g.user_id, 

106 name=name, 

107 description=data.get('description', ''), 

108 url=data.get('url', ''), 

109 category=data.get('category', 'general'), 

110 ) 

111 db.add(server) 

112 db.flush() # get server.id 

113 

114 # Register tools if provided 

115 tools_data = data.get('tools', []) 

116 for td in tools_data[:50]: # cap at 50 tools per server 

117 tool = MCPTool( 

118 server_id=server.id, 

119 name=td.get('name', 'unnamed'), 

120 description=td.get('description', ''), 

121 input_schema=td.get('input_schema', {}), 

122 ) 

123 db.add(tool) 

124 

125 db.commit() 

126 db.refresh(server) 

127 return _ok(server.to_dict(), status=201) 

128 except Exception as e: 

129 db.rollback() 

130 logger.error(f"mcp/register POST error: {e}") 

131 return _err(str(e), 500) 

132 finally: 

133 db.close() 

134 

135 

136@mcp_bp.route('/mcp/discover', methods=['GET']) 

137@optional_auth 

138def discover_mcp(): 

139 """Discover MCP servers and tools by search query.""" 

140 db = get_db() 

141 try: 

142 q = request.args.get('q', '').strip() 

143 limit = min(int(request.args.get('limit', 20)), 50) 

144 

145 if not q: 

146 return _err('q parameter required') 

147 

148 # Search servers 

149 servers = db.query(MCPServer).filter( 

150 MCPServer.is_active == True, 

151 MCPServer.name.ilike(f'%{q}%') | 

152 MCPServer.description.ilike(f'%{q}%') 

153 ).limit(limit).all() 

154 

155 # Search tools 

156 tools = db.query(MCPTool).filter( 

157 MCPTool.name.ilike(f'%{q}%') | 

158 MCPTool.description.ilike(f'%{q}%') 

159 ).limit(limit).all() 

160 

161 return _ok({ 

162 'servers': [s.to_dict() for s in servers], 

163 'tools': [t.to_dict() for t in tools], 

164 }) 

165 except Exception as e: 

166 logger.error(f"mcp/discover GET error: {e}") 

167 return _err(str(e), 500) 

168 finally: 

169 db.close()