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