traia-iatp 0.1.1__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.

Potentially problematic release.


This version of traia-iatp might be problematic. Click here for more details.

Files changed (72) hide show
  1. traia_iatp/README.md +368 -0
  2. traia_iatp/__init__.py +30 -0
  3. traia_iatp/cli/__init__.py +5 -0
  4. traia_iatp/cli/main.py +483 -0
  5. traia_iatp/client/__init__.py +10 -0
  6. traia_iatp/client/a2a_client.py +274 -0
  7. traia_iatp/client/crewai_a2a_tools.py +335 -0
  8. traia_iatp/client/grpc_a2a_tools.py +349 -0
  9. traia_iatp/client/root_path_a2a_client.py +1 -0
  10. traia_iatp/core/__init__.py +43 -0
  11. traia_iatp/core/models.py +161 -0
  12. traia_iatp/mcp/__init__.py +15 -0
  13. traia_iatp/mcp/client.py +201 -0
  14. traia_iatp/mcp/mcp_agent_template.py +422 -0
  15. traia_iatp/mcp/templates/Dockerfile.j2 +56 -0
  16. traia_iatp/mcp/templates/README.md.j2 +212 -0
  17. traia_iatp/mcp/templates/cursor-rules.md.j2 +326 -0
  18. traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
  19. traia_iatp/mcp/templates/docker-compose.yml.j2 +23 -0
  20. traia_iatp/mcp/templates/dockerignore.j2 +47 -0
  21. traia_iatp/mcp/templates/gitignore.j2 +77 -0
  22. traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
  23. traia_iatp/mcp/templates/pyproject.toml.j2 +26 -0
  24. traia_iatp/mcp/templates/run_local_docker.sh.j2 +94 -0
  25. traia_iatp/mcp/templates/server.py.j2 +240 -0
  26. traia_iatp/mcp/traia_mcp_adapter.py +381 -0
  27. traia_iatp/preview_diagrams.html +181 -0
  28. traia_iatp/registry/__init__.py +26 -0
  29. traia_iatp/registry/atlas_search_indexes.json +280 -0
  30. traia_iatp/registry/embeddings.py +298 -0
  31. traia_iatp/registry/iatp_search_api.py +839 -0
  32. traia_iatp/registry/mongodb_registry.py +771 -0
  33. traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
  34. traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
  35. traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
  36. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
  37. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
  38. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
  39. traia_iatp/registry/readmes/README.md +251 -0
  40. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
  41. traia_iatp/server/__init__.py +15 -0
  42. traia_iatp/server/a2a_server.py +215 -0
  43. traia_iatp/server/example_template_usage.py +72 -0
  44. traia_iatp/server/iatp_server_agent_generator.py +237 -0
  45. traia_iatp/server/iatp_server_template_generator.py +235 -0
  46. traia_iatp/server/templates/Dockerfile.j2 +49 -0
  47. traia_iatp/server/templates/README.md +137 -0
  48. traia_iatp/server/templates/README.md.j2 +425 -0
  49. traia_iatp/server/templates/__init__.py +1 -0
  50. traia_iatp/server/templates/__main__.py.j2 +450 -0
  51. traia_iatp/server/templates/agent.py.j2 +80 -0
  52. traia_iatp/server/templates/agent_config.json.j2 +22 -0
  53. traia_iatp/server/templates/agent_executor.py.j2 +264 -0
  54. traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
  55. traia_iatp/server/templates/env.example.j2 +67 -0
  56. traia_iatp/server/templates/gitignore.j2 +78 -0
  57. traia_iatp/server/templates/grpc_server.py.j2 +218 -0
  58. traia_iatp/server/templates/pyproject.toml.j2 +76 -0
  59. traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
  60. traia_iatp/server/templates/server.py.j2 +190 -0
  61. traia_iatp/special_agencies/__init__.py +4 -0
  62. traia_iatp/special_agencies/registry_search_agency.py +392 -0
  63. traia_iatp/utils/__init__.py +10 -0
  64. traia_iatp/utils/docker_utils.py +251 -0
  65. traia_iatp/utils/general.py +64 -0
  66. traia_iatp/utils/iatp_utils.py +126 -0
  67. traia_iatp-0.1.1.dist-info/METADATA +414 -0
  68. traia_iatp-0.1.1.dist-info/RECORD +72 -0
  69. traia_iatp-0.1.1.dist-info/WHEEL +5 -0
  70. traia_iatp-0.1.1.dist-info/entry_points.txt +2 -0
  71. traia_iatp-0.1.1.dist-info/licenses/LICENSE +21 -0
  72. traia_iatp-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,839 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ IATP Search API
4
+
5
+ This module provides high-level search API functions for finding MCP servers and utility agents
6
+ in the IATP registry using text search, Atlas Search, and vector search capabilities.
7
+ """
8
+
9
+ import asyncio
10
+ from typing import List, Dict, Any, Optional
11
+ from dataclasses import dataclass
12
+ import os
13
+ import logging
14
+
15
+ from pymongo import MongoClient
16
+ from pymongo import server_api
17
+
18
+ # Import for embeddings
19
+ from .embeddings import get_embedding_service
20
+
21
+ # Get environment variables
22
+ CLUSTER_URI = "traia-iatp-cluster.yzwjvgd.mongodb.net/?retryWrites=true&w=majority&appName=Traia-IATP-Cluster"
23
+ DATABASE_NAME = "iatp"
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class MCPServerInfo:
30
+ """Information about an MCP server from the registry."""
31
+ id: str
32
+ name: str
33
+ url: str
34
+ description: str
35
+ server_type: str
36
+ capabilities: List[str]
37
+ metadata: Dict[str, Any]
38
+ tags: List[str]
39
+
40
+ @classmethod
41
+ def from_registry_doc(cls, doc: Dict[str, Any]) -> 'MCPServerInfo':
42
+ """Create MCPServerInfo from MongoDB document."""
43
+ # Extract tags from metadata if present
44
+ metadata = doc.get('metadata', {})
45
+ tags = metadata.get('tags', [])
46
+
47
+ return cls(
48
+ id=str(doc.get('_id', '')),
49
+ name=doc.get('name', ''),
50
+ url=doc.get('url', ''),
51
+ description=doc.get('description', ''),
52
+ server_type=doc.get('server_type', 'streamable-http'),
53
+ capabilities=doc.get('capabilities', []),
54
+ metadata=metadata,
55
+ tags=tags
56
+ )
57
+
58
+
59
+ @dataclass
60
+ class UtilityAgentInfo:
61
+ """Information about a utility agent from the registry."""
62
+ agent_id: str
63
+ name: str
64
+ description: str
65
+ base_url: str
66
+ capabilities: List[str]
67
+ tags: List[str]
68
+ is_active: bool
69
+ metadata: Dict[str, Any]
70
+ skills: List[Dict[str, Any]]
71
+ endpoints: Optional[Dict[str, Any]] = None
72
+
73
+ @classmethod
74
+ def from_registry_doc(cls, doc: Dict[str, Any]) -> 'UtilityAgentInfo':
75
+ """Create UtilityAgentInfo from MongoDB document."""
76
+ return cls(
77
+ agent_id=doc.get('agent_id', ''),
78
+ name=doc.get('name', ''),
79
+ description=doc.get('description', ''),
80
+ base_url=doc.get('base_url', ''),
81
+ capabilities=doc.get('capabilities', []),
82
+ tags=doc.get('tags', []),
83
+ is_active=doc.get('is_active', True),
84
+ metadata=doc.get('metadata', {}),
85
+ skills=doc.get('skills', []),
86
+ endpoints=doc.get('endpoints') # Get endpoints from root level
87
+ )
88
+
89
+
90
+ def get_readonly_connection_string() -> str:
91
+ """Get read-only MongoDB connection string."""
92
+ # Try X.509 certificate authentication first
93
+ cert_file = os.getenv("MONGODB_X509_CERT_FILE")
94
+ if cert_file and os.path.exists(cert_file):
95
+ # Extract just the cluster hostname without query parameters
96
+ cluster_host = CLUSTER_URI.split('?')[0]
97
+ return f"mongodb+srv://{cluster_host}?authSource=$external&authMechanism=MONGODB-X509&tls=true&tlsCertificateKeyFile={cert_file}"
98
+
99
+ # Fallback to username/password authentication
100
+ user = os.getenv("MONGODB_USER")
101
+ password = os.getenv("MONGODB_PASSWORD")
102
+ if user and password:
103
+ logger.info("Using username/password authentication for read-only access")
104
+ return f"mongodb+srv://{user}:{password}@{CLUSTER_URI}"
105
+
106
+ # Try connection string as last resort
107
+ conn_str = os.getenv("MONGODB_CONNECTION_STRING")
108
+ if conn_str:
109
+ return conn_str
110
+
111
+ raise ValueError(
112
+ "MongoDB authentication required. Please provide either:\n"
113
+ "1. MONGODB_X509_CERT_FILE - Path to X.509 certificate file\n"
114
+ "2. MONGODB_USER and MONGODB_PASSWORD - Username and password\n"
115
+ "3. MONGODB_CONNECTION_STRING - Full connection string"
116
+ )
117
+
118
+
119
+ def get_collection_names():
120
+ """Get environment-specific collection names."""
121
+ env = os.getenv("ENV", "test").lower()
122
+
123
+ # Validate environment
124
+ valid_envs = ["test", "staging", "prod"]
125
+ if env not in valid_envs:
126
+ logger.warning(f"Invalid ENV '{env}', defaulting to 'test'. Valid values: {valid_envs}")
127
+ env = "test"
128
+
129
+ return {
130
+ "utility_agent": f"iatp-utility-agent-registry-{env}",
131
+ "mcp_server": f"iatp-mcp-server-registry-{env}"
132
+ }
133
+
134
+
135
+ class IATPSearchAPI:
136
+ """
137
+ Search API for finding MCP servers and utility agents in the IATP registry.
138
+ Provides text search, Atlas Search, and vector search capabilities.
139
+ """
140
+
141
+ # Class variable to cache readonly connections
142
+ _client = None
143
+ _db = None
144
+
145
+ @classmethod
146
+ def _get_connection(cls):
147
+ """Get or create read-only MongoDB connection."""
148
+ if cls._client is None:
149
+ conn_str = get_readonly_connection_string()
150
+ cls._client = MongoClient(conn_str, server_api=server_api.ServerApi('1'))
151
+ cls._db = cls._client[DATABASE_NAME]
152
+ return cls._db
153
+
154
+ @classmethod
155
+ def find_utility_agent(
156
+ cls,
157
+ name: Optional[str] = None,
158
+ agent_id: Optional[str] = None,
159
+ capability: Optional[str] = None,
160
+ tag: Optional[str] = None,
161
+ query: Optional[str] = None
162
+ ) -> Optional[UtilityAgentInfo]:
163
+ """
164
+ Find a utility agent in the registry.
165
+
166
+ Args:
167
+ name: Exact name of the agent
168
+ agent_id: Exact agent_id of the agent
169
+ capability: Find agents with this capability
170
+ tag: Find agents with this tag
171
+ query: Text search query (uses Atlas Search)
172
+
173
+ Returns:
174
+ UtilityAgentInfo if found, None otherwise
175
+ """
176
+ db = cls._get_connection()
177
+ collection_names = get_collection_names()
178
+ collection = db[collection_names["utility_agent"]]
179
+
180
+ if agent_id:
181
+ # Direct lookup by agent_id
182
+ doc = collection.find_one({"agent_id": agent_id, "is_active": True})
183
+ if doc:
184
+ return UtilityAgentInfo.from_registry_doc(doc)
185
+
186
+ if name:
187
+ # Direct lookup by name
188
+ doc = collection.find_one({"name": name, "is_active": True})
189
+ if doc:
190
+ return UtilityAgentInfo.from_registry_doc(doc)
191
+
192
+ # If query is provided, use Atlas Search
193
+ if query:
194
+ env = os.getenv("ENV", "test").lower()
195
+ atlas_index_name = f"utility_agent_atlas_search_{env}"
196
+
197
+ pipeline = [
198
+ {
199
+ "$search": {
200
+ "index": atlas_index_name,
201
+ "text": {
202
+ "query": query,
203
+ "path": ["name", "description", "tags", "capabilities", "skills.name", "skills.description", "skills.tags"]
204
+ }
205
+ }
206
+ },
207
+ {"$match": {"is_active": True}},
208
+ ]
209
+
210
+ # Add additional filters if provided
211
+ match_filters = {}
212
+ if capability:
213
+ match_filters["capabilities"] = capability
214
+ if tag:
215
+ match_filters["tags"] = tag
216
+
217
+ if match_filters:
218
+ pipeline.append({"$match": match_filters})
219
+
220
+ pipeline.append({"$limit": 1})
221
+
222
+ results = list(collection.aggregate(pipeline))
223
+ if results:
224
+ return UtilityAgentInfo.from_registry_doc(results[0])
225
+ else:
226
+ # Build standard query filters
227
+ filters = {"is_active": True}
228
+
229
+ if capability:
230
+ filters["capabilities"] = capability
231
+
232
+ if tag:
233
+ filters["tags"] = tag
234
+
235
+ # Find first matching document
236
+ doc = collection.find_one(filters)
237
+ if doc:
238
+ return UtilityAgentInfo.from_registry_doc(doc)
239
+
240
+ return None
241
+
242
+ @classmethod
243
+ def list_utility_agents(
244
+ cls,
245
+ limit: int = 10,
246
+ tags: Optional[List[str]] = None,
247
+ capabilities: Optional[List[str]] = None,
248
+ active_only: bool = True
249
+ ) -> List[UtilityAgentInfo]:
250
+ """
251
+ List available utility agents from the registry.
252
+
253
+ Args:
254
+ limit: Maximum number of agents to return
255
+ tags: Filter by tags
256
+ capabilities: Filter by capabilities
257
+ active_only: Only return active agents
258
+
259
+ Returns:
260
+ List of UtilityAgentInfo objects
261
+ """
262
+ db = cls._get_connection()
263
+ collection_names = get_collection_names()
264
+ collection = db[collection_names["utility_agent"]]
265
+
266
+ # Build query filters
267
+ filters = {}
268
+ if active_only:
269
+ filters["is_active"] = True
270
+ if tags:
271
+ filters["tags"] = {"$in": tags}
272
+ if capabilities:
273
+ filters["capabilities"] = {"$in": capabilities}
274
+
275
+ # Query with limit
276
+ cursor = collection.find(filters).limit(limit).sort("registered_at", -1)
277
+
278
+ return [UtilityAgentInfo.from_registry_doc(doc) for doc in cursor]
279
+
280
+ @classmethod
281
+ async def search_utility_agents(
282
+ cls,
283
+ query: str,
284
+ limit: int = 10,
285
+ active_only: bool = True,
286
+ embedding_fields: Optional[List[str]] = None
287
+ ) -> List[UtilityAgentInfo]:
288
+ """
289
+ Search utility agents using vector search.
290
+
291
+ Args:
292
+ query: Search query
293
+ limit: Maximum number of results
294
+ active_only: Only return active agents
295
+ embedding_fields: Specific embedding fields to search on. If None, uses only search_text.
296
+ Options: ["description", "tags", "capabilities", "agent_card"]
297
+
298
+ Returns:
299
+ List of UtilityAgentInfo objects
300
+ """
301
+ # Generate query embedding
302
+ try:
303
+ embedding_service = get_embedding_service()
304
+ query_embedding = await embedding_service.generate_query_embedding(query)
305
+ except Exception as e:
306
+ logger.error(f"Failed to generate query embedding: {e}")
307
+ # Fallback to Atlas text search
308
+ return await cls._fallback_atlas_search_agents(query, limit, active_only)
309
+
310
+ db = cls._get_connection()
311
+ collection_names = get_collection_names()
312
+ collection = db[collection_names["utility_agent"]]
313
+
314
+ env = os.getenv("ENV", "test").lower()
315
+ vector_index_name = f"utility_agent_vector_search_{env}"
316
+
317
+ # Default to search_text only, or use specified fields
318
+ if embedding_fields is None:
319
+ fields_to_search = ["embeddings.search_text"]
320
+ else:
321
+ # Map field names to embedding paths
322
+ field_mapping = {
323
+ "description": "embeddings.description",
324
+ "tags": "embeddings.tags",
325
+ "capabilities": "embeddings.capabilities",
326
+ "agent_card": "embeddings.agent_card"
327
+ }
328
+ fields_to_search = [field_mapping.get(f, f"embeddings.{f}") for f in embedding_fields]
329
+
330
+ # If only one field, do a simple search
331
+ if len(fields_to_search) == 1:
332
+ pipeline = [
333
+ {
334
+ "$vectorSearch": {
335
+ "index": vector_index_name,
336
+ "path": fields_to_search[0],
337
+ "queryVector": query_embedding,
338
+ "numCandidates": limit * 5,
339
+ "limit": limit,
340
+ "filter": {"is_active": True} if active_only else {}
341
+ }
342
+ },
343
+ {
344
+ "$project": {
345
+ "_id": 0,
346
+ "agent_id": 1,
347
+ "name": 1,
348
+ "description": 1,
349
+ "base_url": 1,
350
+ "capabilities": 1,
351
+ "tags": 1,
352
+ "is_active": 1,
353
+ "metadata": 1,
354
+ "skills": 1,
355
+ "score": {"$meta": "vectorSearchScore"}
356
+ }
357
+ }
358
+ ]
359
+
360
+ results = list(collection.aggregate(pipeline))
361
+ return [UtilityAgentInfo.from_registry_doc(doc) for doc in results]
362
+
363
+ # Multiple fields - aggregate results
364
+ all_results = []
365
+ seen_ids = set()
366
+
367
+ for field in fields_to_search:
368
+ pipeline = [
369
+ {
370
+ "$vectorSearch": {
371
+ "index": vector_index_name,
372
+ "path": field,
373
+ "queryVector": query_embedding,
374
+ "numCandidates": limit * 5,
375
+ "limit": limit,
376
+ "filter": {"is_active": True} if active_only else {}
377
+ }
378
+ },
379
+ {
380
+ "$project": {
381
+ "_id": 0,
382
+ "agent_id": 1,
383
+ "name": 1,
384
+ "description": 1,
385
+ "base_url": 1,
386
+ "capabilities": 1,
387
+ "tags": 1,
388
+ "is_active": 1,
389
+ "metadata": 1,
390
+ "skills": 1,
391
+ "score": {"$meta": "vectorSearchScore"}
392
+ }
393
+ }
394
+ ]
395
+
396
+ results = list(collection.aggregate(pipeline))
397
+
398
+ # Add results avoiding duplicates
399
+ for doc in results:
400
+ if doc.get("agent_id") not in seen_ids:
401
+ seen_ids.add(doc.get("agent_id"))
402
+ all_results.append(doc)
403
+
404
+ # Sort by score (highest first) and limit
405
+ all_results.sort(key=lambda x: x.get("score", 0), reverse=True)
406
+ all_results = all_results[:limit]
407
+
408
+ # Convert to UtilityAgentInfo objects
409
+ return [UtilityAgentInfo.from_registry_doc(doc) for doc in all_results]
410
+
411
+ @classmethod
412
+ async def _fallback_atlas_search_agents(
413
+ cls,
414
+ query: str,
415
+ limit: int,
416
+ active_only: bool
417
+ ) -> List[UtilityAgentInfo]:
418
+ """Fallback to Atlas text search if vector search fails."""
419
+ db = cls._get_connection()
420
+ collection_names = get_collection_names()
421
+ collection = db[collection_names["utility_agent"]]
422
+
423
+ env = os.getenv("ENV", "test").lower()
424
+ atlas_index_name = f"utility_agent_atlas_search_{env}"
425
+
426
+ pipeline = [
427
+ {
428
+ "$search": {
429
+ "index": atlas_index_name,
430
+ "text": {
431
+ "query": query,
432
+ "path": ["name", "description", "tags", "capabilities", "skills.name", "skills.description", "skills.tags"]
433
+ }
434
+ }
435
+ }
436
+ ]
437
+
438
+ if active_only:
439
+ pipeline.append({"$match": {"is_active": True}})
440
+
441
+ pipeline.append({"$limit": limit})
442
+
443
+ results = list(collection.aggregate(pipeline))
444
+ return [UtilityAgentInfo.from_registry_doc(doc) for doc in results]
445
+
446
+ @classmethod
447
+ def find_mcp_server(
448
+ cls,
449
+ name: Optional[str] = None,
450
+ capability: Optional[str] = None,
451
+ tag: Optional[str] = None,
452
+ query: Optional[str] = None
453
+ ) -> Optional[MCPServerInfo]:
454
+ """
455
+ Find an MCP server in the registry.
456
+
457
+ Args:
458
+ name: Exact name of the MCP server
459
+ capability: Find servers with this capability
460
+ tag: Find servers with this tag
461
+ query: Text search query (uses Atlas Search)
462
+
463
+ Returns:
464
+ MCPServerInfo if found, None otherwise
465
+ """
466
+ db = cls._get_connection()
467
+ collection_names = get_collection_names()
468
+ collection = db[collection_names["mcp_server"]]
469
+
470
+ if name:
471
+ # Direct lookup by name
472
+ doc = collection.find_one({"name": name, "is_active": True})
473
+ if doc:
474
+ return MCPServerInfo.from_registry_doc(doc)
475
+
476
+ # If query is provided, use Atlas Search
477
+ if query:
478
+ env = os.getenv("ENV", "test").lower()
479
+ atlas_index_name = f"mcp_server_atlas_search_{env}"
480
+
481
+ pipeline = [
482
+ {
483
+ "$search": {
484
+ "index": atlas_index_name,
485
+ "text": {
486
+ "query": query,
487
+ "path": ["name", "description", "capabilities"]
488
+ }
489
+ }
490
+ },
491
+ {"$match": {"is_active": True}},
492
+ ]
493
+
494
+ # Add additional filters if provided
495
+ match_filters = {}
496
+ if capability:
497
+ match_filters["capabilities"] = capability
498
+ if tag:
499
+ # Tags are stored in metadata
500
+ match_filters["metadata.tags"] = tag
501
+
502
+ if match_filters:
503
+ pipeline.append({"$match": match_filters})
504
+
505
+ pipeline.append({"$limit": 1})
506
+
507
+ results = list(collection.aggregate(pipeline))
508
+ if results:
509
+ return MCPServerInfo.from_registry_doc(results[0])
510
+ else:
511
+ # Build standard query filters
512
+ filters = {"is_active": True}
513
+
514
+ if capability:
515
+ filters["capabilities"] = capability
516
+
517
+ if tag:
518
+ # Tags are stored in metadata
519
+ filters["metadata.tags"] = tag
520
+
521
+ # Find first matching document
522
+ doc = collection.find_one(filters)
523
+ if doc:
524
+ return MCPServerInfo.from_registry_doc(doc)
525
+
526
+ return None
527
+
528
+ @classmethod
529
+ def list_mcp_servers(
530
+ cls,
531
+ limit: int = 10,
532
+ tags: Optional[List[str]] = None,
533
+ capabilities: Optional[List[str]] = None
534
+ ) -> List[MCPServerInfo]:
535
+ """
536
+ List available MCP servers from the registry.
537
+
538
+ Args:
539
+ limit: Maximum number of servers to return
540
+ tags: Filter by tags
541
+ capabilities: Filter by capabilities
542
+
543
+ Returns:
544
+ List of MCPServerInfo objects
545
+ """
546
+ db = cls._get_connection()
547
+ collection_names = get_collection_names()
548
+ collection = db[collection_names["mcp_server"]]
549
+
550
+ # Build query filters
551
+ filters = {"is_active": True}
552
+ if capabilities:
553
+ filters["capabilities"] = {"$in": capabilities}
554
+
555
+ # Query
556
+ cursor = collection.find(filters).limit(limit).sort("registered_at", -1)
557
+
558
+ # Manual filtering for tags if needed
559
+ servers = []
560
+ for doc in cursor:
561
+ if tags:
562
+ doc_tags = doc.get("metadata", {}).get("tags", [])
563
+ if not any(tag in doc_tags for tag in tags):
564
+ continue
565
+ servers.append(MCPServerInfo.from_registry_doc(doc))
566
+
567
+ return servers[:limit]
568
+
569
+ @classmethod
570
+ def get_mcp_server(cls, name: str) -> Optional[Dict[str, Any]]:
571
+ """
572
+ Get an MCP server by name (returns raw document).
573
+
574
+ Args:
575
+ name: Name of the MCP server
576
+
577
+ Returns:
578
+ MongoDB document if found, None otherwise
579
+ """
580
+ db = cls._get_connection()
581
+ collection_names = get_collection_names()
582
+ collection = db[collection_names["mcp_server"]]
583
+
584
+ doc = collection.find_one({"name": name})
585
+ if doc:
586
+ doc["_id"] = str(doc["_id"])
587
+ return doc
588
+
589
+ @classmethod
590
+ def close_connections(cls):
591
+ """Close all registry connections."""
592
+ if cls._client:
593
+ cls._client.close()
594
+ cls._client = None
595
+ cls._db = None
596
+
597
+ @classmethod
598
+ async def search_mcp_servers(
599
+ cls,
600
+ query: str,
601
+ limit: int = 10,
602
+ active_only: bool = True,
603
+ embedding_fields: Optional[List[str]] = None
604
+ ) -> List[MCPServerInfo]:
605
+ """
606
+ Search MCP servers using vector search.
607
+
608
+ Args:
609
+ query: Search query
610
+ limit: Maximum number of results
611
+ active_only: Only return active servers
612
+ embedding_fields: Specific embedding fields to search on. If None, searches both description and capabilities.
613
+ Options: ["description", "capabilities"]
614
+
615
+ Returns:
616
+ List of MCPServerInfo objects
617
+ """
618
+ # Generate query embedding
619
+ try:
620
+ embedding_service = get_embedding_service()
621
+ query_embedding = await embedding_service.generate_query_embedding(query)
622
+ except Exception as e:
623
+ logger.error(f"Failed to generate query embedding: {e}")
624
+ # Fallback to Atlas text search
625
+ return await cls._fallback_atlas_search_mcp_servers(query, limit, active_only)
626
+
627
+ db = cls._get_connection()
628
+ collection_names = get_collection_names()
629
+ collection = db[collection_names["mcp_server"]]
630
+
631
+ env = os.getenv("ENV", "test").lower()
632
+ vector_index_name = f"mcp_server_vector_search_{env}"
633
+
634
+ # Default to both fields, or use specified fields
635
+ if embedding_fields is None:
636
+ fields_to_search = ["description_embedding", "capabilities_embedding"]
637
+ else:
638
+ # Map field names to embedding paths
639
+ field_mapping = {
640
+ "description": "description_embedding",
641
+ "capabilities": "capabilities_embedding"
642
+ }
643
+ fields_to_search = [field_mapping.get(f, f"{f}_embedding") for f in embedding_fields]
644
+
645
+ # If only one field, do a simple search
646
+ if len(fields_to_search) == 1:
647
+ pipeline = [
648
+ {
649
+ "$vectorSearch": {
650
+ "index": vector_index_name,
651
+ "path": fields_to_search[0],
652
+ "queryVector": query_embedding,
653
+ "numCandidates": limit * 5,
654
+ "limit": limit,
655
+ "filter": {"is_active": True} if active_only else {}
656
+ }
657
+ },
658
+ {
659
+ "$project": {
660
+ "_id": 0,
661
+ "name": 1,
662
+ "url": 1,
663
+ "description": 1,
664
+ "server_type": 1,
665
+ "capabilities": 1,
666
+ "metadata": 1,
667
+ "registered_at": 1,
668
+ "is_active": 1,
669
+ "score": {"$meta": "vectorSearchScore"}
670
+ }
671
+ }
672
+ ]
673
+
674
+ results = list(collection.aggregate(pipeline))
675
+ return [MCPServerInfo.from_registry_doc(doc) for doc in results]
676
+
677
+ # Multiple fields - aggregate results
678
+ all_results = []
679
+ seen_names = set()
680
+
681
+ for field in fields_to_search:
682
+ pipeline = [
683
+ {
684
+ "$vectorSearch": {
685
+ "index": vector_index_name,
686
+ "path": field,
687
+ "queryVector": query_embedding,
688
+ "numCandidates": limit * 5,
689
+ "limit": limit,
690
+ "filter": {"is_active": True} if active_only else {}
691
+ }
692
+ },
693
+ {
694
+ "$project": {
695
+ "_id": 0,
696
+ "name": 1,
697
+ "url": 1,
698
+ "description": 1,
699
+ "server_type": 1,
700
+ "capabilities": 1,
701
+ "metadata": 1,
702
+ "registered_at": 1,
703
+ "is_active": 1,
704
+ "score": {"$meta": "vectorSearchScore"}
705
+ }
706
+ }
707
+ ]
708
+
709
+ results = list(collection.aggregate(pipeline))
710
+
711
+ # Add results avoiding duplicates
712
+ for doc in results:
713
+ if doc.get("name") not in seen_names:
714
+ seen_names.add(doc.get("name"))
715
+ all_results.append(doc)
716
+
717
+ # Sort by score (highest first) and limit
718
+ all_results.sort(key=lambda x: x.get("score", 0), reverse=True)
719
+ all_results = all_results[:limit]
720
+
721
+ # Convert to MCPServerInfo objects
722
+ return [MCPServerInfo.from_registry_doc(doc) for doc in all_results]
723
+
724
+ @classmethod
725
+ async def _fallback_atlas_search_mcp_servers(
726
+ cls,
727
+ query: str,
728
+ limit: int,
729
+ active_only: bool
730
+ ) -> List[MCPServerInfo]:
731
+ """Fallback to Atlas text search if vector search fails."""
732
+ db = cls._get_connection()
733
+ collection_names = get_collection_names()
734
+ collection = db[collection_names["mcp_server"]]
735
+
736
+ env = os.getenv("ENV", "test").lower()
737
+ atlas_index_name = f"mcp_server_atlas_search_{env}"
738
+
739
+ pipeline = [
740
+ {
741
+ "$search": {
742
+ "index": atlas_index_name,
743
+ "text": {
744
+ "query": query,
745
+ "path": ["name", "description", "capabilities"]
746
+ }
747
+ }
748
+ }
749
+ ]
750
+
751
+ if active_only:
752
+ pipeline.append({"$match": {"is_active": True}})
753
+
754
+ pipeline.append({"$limit": limit})
755
+
756
+ results = list(collection.aggregate(pipeline))
757
+ return [MCPServerInfo.from_registry_doc(doc) for doc in results]
758
+
759
+
760
+ # Keep backward compatibility - use IATPSearchAPI
761
+ class RegistryAPI(IATPSearchAPI):
762
+ """Backward compatibility alias for IATPSearchAPI."""
763
+ pass
764
+
765
+
766
+ # Convenience functions that don't require class instantiation
767
+ def find_utility_agent(
768
+ name: Optional[str] = None,
769
+ agent_id: Optional[str] = None,
770
+ capability: Optional[str] = None,
771
+ tag: Optional[str] = None,
772
+ query: Optional[str] = None
773
+ ) -> Optional[UtilityAgentInfo]:
774
+ """Find a utility agent in the registry."""
775
+ return IATPSearchAPI.find_utility_agent(name, agent_id, capability, tag, query)
776
+
777
+
778
+ def list_utility_agents(
779
+ limit: int = 10,
780
+ tags: Optional[List[str]] = None,
781
+ capabilities: Optional[List[str]] = None,
782
+ active_only: bool = True
783
+ ) -> List[UtilityAgentInfo]:
784
+ """List available utility agents from the registry."""
785
+ return IATPSearchAPI.list_utility_agents(limit, tags, capabilities, active_only)
786
+
787
+
788
+ async def search_utility_agents(
789
+ query: str,
790
+ limit: int = 10,
791
+ active_only: bool = True,
792
+ embedding_fields: Optional[List[str]] = None
793
+ ) -> List[UtilityAgentInfo]:
794
+ """Search utility agents using vector search."""
795
+ return await IATPSearchAPI.search_utility_agents(query, limit, active_only, embedding_fields)
796
+
797
+
798
+ def find_mcp_server(
799
+ name: Optional[str] = None,
800
+ capability: Optional[str] = None,
801
+ tag: Optional[str] = None,
802
+ query: Optional[str] = None
803
+ ) -> Optional[MCPServerInfo]:
804
+ """Find an MCP server in the registry."""
805
+ return IATPSearchAPI.find_mcp_server(name, capability, tag, query)
806
+
807
+
808
+ def list_mcp_servers(
809
+ limit: int = 10,
810
+ tags: Optional[List[str]] = None,
811
+ capabilities: Optional[List[str]] = None
812
+ ) -> List[MCPServerInfo]:
813
+ """List available MCP servers from the registry."""
814
+ return IATPSearchAPI.list_mcp_servers(limit, tags, capabilities)
815
+
816
+
817
+ async def search_mcp_servers(
818
+ query: str,
819
+ limit: int = 10,
820
+ embedding_fields: Optional[List[str]] = None
821
+ ) -> List[MCPServerInfo]:
822
+ """
823
+ Search MCP servers using vector search.
824
+
825
+ Args:
826
+ query: Search query
827
+ limit: Maximum number of results
828
+ embedding_fields: Specific embedding fields to search on. If None, searches both description and capabilities.
829
+ Options: ["description", "capabilities"]
830
+
831
+ Returns:
832
+ List of MCPServerInfo objects
833
+ """
834
+ return await IATPSearchAPI.search_mcp_servers(query, limit, active_only=True, embedding_fields=embedding_fields)
835
+
836
+
837
+ def get_mcp_server(name: str) -> Optional[Dict[str, Any]]:
838
+ """Get an MCP server by name (returns raw document)."""
839
+ return IATPSearchAPI.get_mcp_server(name)