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.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +54 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/d402_a2a_client.py +293 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/contracts/__init__.py +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +255 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +172 -0
- traia_iatp/d402/__init__.py +55 -0
- traia_iatp/d402/chains.py +102 -0
- traia_iatp/d402/client.py +150 -0
- traia_iatp/d402/clients/__init__.py +7 -0
- traia_iatp/d402/clients/base.py +218 -0
- traia_iatp/d402/clients/httpx.py +219 -0
- traia_iatp/d402/common.py +114 -0
- traia_iatp/d402/encoding.py +28 -0
- traia_iatp/d402/examples/client_example.py +197 -0
- traia_iatp/d402/examples/server_example.py +171 -0
- traia_iatp/d402/facilitator.py +453 -0
- traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
- traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
- traia_iatp/d402/fastmcp_middleware.py +147 -0
- traia_iatp/d402/mcp_middleware.py +434 -0
- traia_iatp/d402/middleware.py +193 -0
- traia_iatp/d402/models.py +116 -0
- traia_iatp/d402/networks.py +98 -0
- traia_iatp/d402/path.py +43 -0
- traia_iatp/d402/payment_introspection.py +104 -0
- traia_iatp/d402/payment_signing.py +178 -0
- traia_iatp/d402/paywall.py +119 -0
- traia_iatp/d402/starlette_middleware.py +326 -0
- traia_iatp/d402/template.py +1 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/__init__.py +18 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
- traia_iatp/mcp/mcp_agent_template.py +481 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
- traia_iatp/mcp/templates/README.md.j2 +310 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/env.example.j2 +57 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
- traia_iatp/mcp/templates/server.py.j2 +175 -0
- traia_iatp/mcp/traia_mcp_adapter.py +543 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +846 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +219 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +565 -0
- traia_iatp/server/templates/agent.py.j2 +94 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +279 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +84 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +78 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +243 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.29.dist-info/METADATA +423 -0
- traia_iatp-0.1.29.dist-info/RECORD +107 -0
- traia_iatp-0.1.29.dist-info/WHEEL +5 -0
- traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
"""MongoDB registry for utility agents and MCP servers (write operations only).
|
|
2
|
+
|
|
3
|
+
This module handles all write operations for the IATP registry:
|
|
4
|
+
- Adding utility agents and MCP servers
|
|
5
|
+
- Updating agent information and health status
|
|
6
|
+
- Managing agent lifecycle (activation/deactivation)
|
|
7
|
+
- Registry statistics
|
|
8
|
+
|
|
9
|
+
For search and query operations, use iatp_search_api.py instead.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
import asyncio
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import List, Optional, Dict, Any
|
|
17
|
+
from pymongo import MongoClient, ASCENDING, TEXT
|
|
18
|
+
from pymongo import server_api
|
|
19
|
+
from pymongo.errors import ServerSelectionTimeoutError, NetworkTimeout, AutoReconnect
|
|
20
|
+
from pymongo.database import Database
|
|
21
|
+
from pymongo.collection import Collection
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
# Handle imports for both module and script usage
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
# When running as a script, import directly
|
|
27
|
+
import sys
|
|
28
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
29
|
+
from src.traia_iatp.core.models import UtilityAgentRegistryEntry, UtilityAgent
|
|
30
|
+
from src.traia_iatp.registry.embeddings import get_embedding_service
|
|
31
|
+
from ..utils import get_now_in_utc
|
|
32
|
+
else:
|
|
33
|
+
# When imported as a module
|
|
34
|
+
from ..core.models import UtilityAgentRegistryEntry, UtilityAgent
|
|
35
|
+
from .embeddings import get_embedding_service
|
|
36
|
+
from ..utils import get_now_in_utc
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
CLUSTER_URI = "traia-iatp-cluster.yzwjvgd.mongodb.net/?retryWrites=true&w=majority&appName=Traia-IATP-Cluster"
|
|
41
|
+
DATABASE_NAME = "iatp"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_collection_names():
|
|
45
|
+
"""Get environment-specific collection names."""
|
|
46
|
+
env = os.getenv("ENV", "test").lower()
|
|
47
|
+
|
|
48
|
+
# Validate environment
|
|
49
|
+
valid_envs = ["test", "staging", "prod"]
|
|
50
|
+
if env not in valid_envs:
|
|
51
|
+
logger.warning(f"Invalid ENV '{env}', defaulting to 'test'. Valid values: {valid_envs}")
|
|
52
|
+
env = "test"
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"utility_agent": f"iatp-utility-agent-registry-{env}",
|
|
56
|
+
"mcp_server": f"iatp-mcp-server-registry-{env}"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_search_index_names():
|
|
61
|
+
"""Get environment-specific search index names."""
|
|
62
|
+
env = os.getenv("ENV", "test").lower()
|
|
63
|
+
|
|
64
|
+
# Validate environment
|
|
65
|
+
valid_envs = ["test", "staging", "prod"]
|
|
66
|
+
if env not in valid_envs:
|
|
67
|
+
env = "test"
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"utility_agent_atlas_search": f"utility_agent_atlas_search_{env}",
|
|
71
|
+
"utility_agent_vector_search": f"utility_agent_vector_search_{env}",
|
|
72
|
+
"mcp_server_atlas_search": f"mcp_server_atlas_search_{env}",
|
|
73
|
+
"mcp_server_vector_search": f"mcp_server_vector_search_{env}"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _create_mongodb_client_with_retry(connection_string: str, max_retries: int = 3) -> MongoClient:
|
|
78
|
+
"""Create MongoDB client with retry logic for connection resilience."""
|
|
79
|
+
for attempt in range(max_retries):
|
|
80
|
+
try:
|
|
81
|
+
client = MongoClient(
|
|
82
|
+
connection_string,
|
|
83
|
+
server_api=server_api.ServerApi('1'),
|
|
84
|
+
serverSelectionTimeoutMS=15000, # 15 second timeout
|
|
85
|
+
connectTimeoutMS=10000, # 10 second connect timeout
|
|
86
|
+
socketTimeoutMS=30000, # 30 second socket timeout
|
|
87
|
+
maxPoolSize=10, # Connection pool size
|
|
88
|
+
retryWrites=True # Enable retryable writes
|
|
89
|
+
)
|
|
90
|
+
# Test the connection
|
|
91
|
+
client.admin.command('ping')
|
|
92
|
+
logger.info(f"MongoDB connection established successfully (attempt {attempt + 1})")
|
|
93
|
+
return client
|
|
94
|
+
except (ServerSelectionTimeoutError, NetworkTimeout, AutoReconnect, Exception) as e:
|
|
95
|
+
error_msg = str(e).lower()
|
|
96
|
+
is_connection_error = any(term in error_msg for term in ['ssl', 'tls', 'handshake', 'timeout', 'network'])
|
|
97
|
+
|
|
98
|
+
if attempt < max_retries - 1 and is_connection_error:
|
|
99
|
+
delay = (2 ** attempt) + (attempt * 0.5) # Exponential backoff with jitter
|
|
100
|
+
logger.warning(f"MongoDB connection attempt {attempt + 1} failed: {e}")
|
|
101
|
+
logger.info(f"Retrying in {delay:.1f} seconds...")
|
|
102
|
+
time.sleep(delay)
|
|
103
|
+
else:
|
|
104
|
+
logger.error(f"All {max_retries} MongoDB connection attempts failed")
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UtilityAgentRegistry:
|
|
109
|
+
"""Cloud MongoDB-based registry for utility agents (write operations only).
|
|
110
|
+
|
|
111
|
+
This class handles all write operations for utility agents:
|
|
112
|
+
- Adding new agents to the registry
|
|
113
|
+
- Updating agent information and health status
|
|
114
|
+
- Managing agent lifecycle
|
|
115
|
+
- Registry statistics
|
|
116
|
+
|
|
117
|
+
For search and query operations, use iatp_search_api.py instead.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, connection_string: Optional[str] = None, database_name: str = DATABASE_NAME):
|
|
121
|
+
"""Initialize MongoDB registry for cloud usage.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
connection_string: MongoDB connection string (should be a cloud MongoDB URI)
|
|
125
|
+
database_name: Name of the database to use
|
|
126
|
+
"""
|
|
127
|
+
if connection_string:
|
|
128
|
+
self.connection_string = connection_string
|
|
129
|
+
else:
|
|
130
|
+
# Try X.509 certificate authentication first
|
|
131
|
+
cert_file = os.getenv("MONGODB_X509_CERT_FILE")
|
|
132
|
+
if cert_file and os.path.exists(cert_file):
|
|
133
|
+
# For X.509 authentication, we need to extract the subject from the certificate
|
|
134
|
+
# to use as the username. MongoDB Atlas typically uses the full DN as username.
|
|
135
|
+
# The connection string format for X.509 is:
|
|
136
|
+
# mongodb+srv://cluster.mongodb.net/?authSource=$external&authMechanism=MONGODB-X509
|
|
137
|
+
# Extract just the cluster hostname without query parameters
|
|
138
|
+
cluster_host = CLUSTER_URI.split('?')[0]
|
|
139
|
+
self.connection_string = f"mongodb+srv://{cluster_host}?authSource=$external&authMechanism=MONGODB-X509&tls=true&tlsCertificateKeyFile={cert_file}"
|
|
140
|
+
logger.info(f"Using X.509 certificate authentication from {cert_file}")
|
|
141
|
+
else:
|
|
142
|
+
# Fallback to username/password authentication
|
|
143
|
+
user = os.getenv("MONGODB_USER")
|
|
144
|
+
password = os.getenv("MONGODB_PASSWORD")
|
|
145
|
+
if user and password:
|
|
146
|
+
self.connection_string = f"mongodb+srv://{user}:{password}@{CLUSTER_URI}"
|
|
147
|
+
logger.info("Using username/password authentication")
|
|
148
|
+
else:
|
|
149
|
+
# Try connection string as last resort
|
|
150
|
+
self.connection_string = os.getenv("MONGODB_CONNECTION_STRING")
|
|
151
|
+
if not self.connection_string:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
"MongoDB authentication required. Please provide either:\n"
|
|
154
|
+
"1. MONGODB_X509_CERT_FILE - Path to X.509 certificate file\n"
|
|
155
|
+
"2. MONGODB_USER and MONGODB_PASSWORD - Username and password\n"
|
|
156
|
+
"3. MONGODB_CONNECTION_STRING - Full connection string"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.database_name = database_name
|
|
160
|
+
self.client = _create_mongodb_client_with_retry(self.connection_string)
|
|
161
|
+
self.db: Database = self.client[self.database_name]
|
|
162
|
+
|
|
163
|
+
# Get environment-specific collection name
|
|
164
|
+
collection_names = get_collection_names()
|
|
165
|
+
self.registry: Collection = self.db[collection_names["utility_agent"]]
|
|
166
|
+
logger.info(f"Using collection: {collection_names['utility_agent']}")
|
|
167
|
+
|
|
168
|
+
# Create indexes for efficient searching (only if they don't exist)
|
|
169
|
+
self._ensure_indexes()
|
|
170
|
+
|
|
171
|
+
def _ensure_indexes(self):
|
|
172
|
+
"""Ensure indexes exist for efficient searching."""
|
|
173
|
+
existing_indexes = [idx['name'] for idx in self.registry.list_indexes()]
|
|
174
|
+
|
|
175
|
+
# NOTE: Atlas Search and Vector Search indexes must be created through Atlas UI or API
|
|
176
|
+
# See atlas_search_indexes.json and ATLAS_SEARCH_SETUP.md for instructions
|
|
177
|
+
|
|
178
|
+
# Only create regular indexes (not text search)
|
|
179
|
+
index_specs = [
|
|
180
|
+
("agent_id", "agent_id_index", True), # unique
|
|
181
|
+
("name", "name_index", True), # unique
|
|
182
|
+
("base_url", "base_url_index", True), # unique - base URL is the primary endpoint
|
|
183
|
+
("is_active", "is_active_index", False),
|
|
184
|
+
("tags", "tags_index", False),
|
|
185
|
+
("capabilities", "capabilities_index", False),
|
|
186
|
+
([("registered_at", ASCENDING)], "registered_at_index", False)
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
for spec, index_name, is_unique in index_specs:
|
|
190
|
+
if index_name not in existing_indexes:
|
|
191
|
+
try:
|
|
192
|
+
if isinstance(spec, list):
|
|
193
|
+
self.registry.create_index(spec, name=index_name)
|
|
194
|
+
else:
|
|
195
|
+
self.registry.create_index(spec, name=index_name, unique=is_unique)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning(f"Could not create index {index_name}: {e}")
|
|
198
|
+
|
|
199
|
+
async def add_utility_agent(self, agent: UtilityAgent, tags: List[str] = None) -> UtilityAgentRegistryEntry:
|
|
200
|
+
"""Add a utility agent to the cloud registry.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
agent: UtilityAgent object with endpoints configured
|
|
204
|
+
tags: Optional additional tags for search
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
UtilityAgentRegistryEntry created or updated
|
|
208
|
+
"""
|
|
209
|
+
# Check if agent with same name already exists
|
|
210
|
+
existing = self.registry.find_one({"name": agent.name})
|
|
211
|
+
|
|
212
|
+
if existing:
|
|
213
|
+
# Update the existing entry with the same name
|
|
214
|
+
agent_id = existing["agent_id"]
|
|
215
|
+
logger.warning(f"Agent with name '{agent.name}' already exists (ID: {agent_id}). Updating it.")
|
|
216
|
+
else:
|
|
217
|
+
agent_id = agent.id
|
|
218
|
+
|
|
219
|
+
# Generate embeddings if enabled
|
|
220
|
+
description_embedding = None
|
|
221
|
+
tags_embedding = None
|
|
222
|
+
capabilities_embedding = None
|
|
223
|
+
agent_card_embedding = None
|
|
224
|
+
search_text_embedding = None
|
|
225
|
+
|
|
226
|
+
if os.getenv("ENABLE_EMBEDDINGS", "true").lower() == "true":
|
|
227
|
+
try:
|
|
228
|
+
embedding_service = get_embedding_service()
|
|
229
|
+
|
|
230
|
+
# Generate embedding for description
|
|
231
|
+
if agent.description:
|
|
232
|
+
description_embedding = await embedding_service.generate_embedding(agent.description)
|
|
233
|
+
|
|
234
|
+
# Generate embedding for tags (concatenated)
|
|
235
|
+
if tags or agent.tags:
|
|
236
|
+
all_tags = list(set((tags or []) + agent.tags))
|
|
237
|
+
tags_text = " ".join(all_tags)
|
|
238
|
+
tags_embedding = await embedding_service.generate_embedding(tags_text)
|
|
239
|
+
|
|
240
|
+
# Generate embedding for capabilities
|
|
241
|
+
if agent.capabilities:
|
|
242
|
+
capabilities_text = " ".join(agent.capabilities)
|
|
243
|
+
capabilities_embedding = await embedding_service.generate_embedding(capabilities_text)
|
|
244
|
+
|
|
245
|
+
# Generate embedding for agent card
|
|
246
|
+
if agent.agent_card:
|
|
247
|
+
# Create comprehensive agent card text
|
|
248
|
+
agent_card_parts = [
|
|
249
|
+
agent.agent_card.name,
|
|
250
|
+
agent.agent_card.description
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
# Add skills information
|
|
254
|
+
for skill in agent.agent_card.skills:
|
|
255
|
+
agent_card_parts.append(skill.name)
|
|
256
|
+
agent_card_parts.append(skill.description)
|
|
257
|
+
agent_card_parts.extend(skill.examples)
|
|
258
|
+
agent_card_parts.extend(skill.tags)
|
|
259
|
+
|
|
260
|
+
agent_card_text = " ".join(filter(None, agent_card_parts))
|
|
261
|
+
agent_card_embedding = await embedding_service.generate_embedding(agent_card_text)
|
|
262
|
+
|
|
263
|
+
# Note: We'll generate search text embedding after creating the entry
|
|
264
|
+
|
|
265
|
+
logger.info(f"Generated embeddings for agent {agent.name}")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.warning(f"Failed to generate embeddings: {e}. Proceeding without embeddings.")
|
|
268
|
+
|
|
269
|
+
# Create entry with enhanced fields
|
|
270
|
+
entry = UtilityAgentRegistryEntry(
|
|
271
|
+
agent_id=agent_id,
|
|
272
|
+
name=agent.name,
|
|
273
|
+
description=agent.description,
|
|
274
|
+
capabilities=agent.capabilities,
|
|
275
|
+
tags=tags or agent.tags,
|
|
276
|
+
metadata=agent.metadata
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Add agent card if available
|
|
280
|
+
if agent.agent_card:
|
|
281
|
+
entry.agent_card = agent.agent_card
|
|
282
|
+
entry.skills = agent.agent_card.skills
|
|
283
|
+
|
|
284
|
+
# Add endpoints if available
|
|
285
|
+
if agent.endpoints:
|
|
286
|
+
entry.endpoints = agent.endpoints
|
|
287
|
+
# Store base_url for indexing since all endpoints are derived from it
|
|
288
|
+
entry.base_url = agent.endpoints.base_url
|
|
289
|
+
|
|
290
|
+
# Generate search text from available data
|
|
291
|
+
search_text_parts = [agent.name, agent.description]
|
|
292
|
+
search_text_parts.extend(agent.capabilities)
|
|
293
|
+
search_text_parts.extend(tags or agent.tags)
|
|
294
|
+
|
|
295
|
+
# Add agent card information to search text
|
|
296
|
+
if agent.agent_card:
|
|
297
|
+
search_text_parts.append(agent.agent_card.name)
|
|
298
|
+
search_text_parts.append(agent.agent_card.description)
|
|
299
|
+
for skill in agent.agent_card.skills:
|
|
300
|
+
search_text_parts.append(skill.name)
|
|
301
|
+
search_text_parts.append(skill.description)
|
|
302
|
+
search_text_parts.extend(skill.examples)
|
|
303
|
+
search_text_parts.extend(skill.tags)
|
|
304
|
+
|
|
305
|
+
# Set the generated search text
|
|
306
|
+
entry.search_text = " ".join(filter(None, search_text_parts))
|
|
307
|
+
|
|
308
|
+
# Generate embedding for the search text we just created
|
|
309
|
+
if os.getenv("ENABLE_EMBEDDINGS", "true").lower() == "true" and entry.search_text:
|
|
310
|
+
try:
|
|
311
|
+
embedding_service = get_embedding_service()
|
|
312
|
+
search_text_embedding = await embedding_service.generate_embedding(entry.search_text)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.warning(f"Failed to generate search text embedding: {e}")
|
|
315
|
+
|
|
316
|
+
# Insert or update in cloud MongoDB
|
|
317
|
+
# Convert to dict and ensure all values are JSON-serializable
|
|
318
|
+
entry_dict = entry.model_dump(mode='json')
|
|
319
|
+
|
|
320
|
+
# Add embeddings if available
|
|
321
|
+
embeddings = {}
|
|
322
|
+
if description_embedding:
|
|
323
|
+
embeddings["description"] = description_embedding
|
|
324
|
+
if tags_embedding:
|
|
325
|
+
embeddings["tags"] = tags_embedding
|
|
326
|
+
if capabilities_embedding:
|
|
327
|
+
embeddings["capabilities"] = capabilities_embedding
|
|
328
|
+
if agent_card_embedding:
|
|
329
|
+
embeddings["agent_card"] = agent_card_embedding
|
|
330
|
+
if search_text_embedding:
|
|
331
|
+
embeddings["search_text"] = search_text_embedding
|
|
332
|
+
|
|
333
|
+
if embeddings:
|
|
334
|
+
entry_dict["embeddings"] = embeddings
|
|
335
|
+
|
|
336
|
+
# Store the full UtilityAgent data as well
|
|
337
|
+
agent_dict = agent.model_dump(mode='json')
|
|
338
|
+
entry_dict["utility_agent_data"] = agent_dict
|
|
339
|
+
|
|
340
|
+
result = self.registry.replace_one(
|
|
341
|
+
{"agent_id": agent_id}, # Use agent_id as the key for upsert
|
|
342
|
+
entry_dict,
|
|
343
|
+
upsert=True
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if result.upserted_id:
|
|
347
|
+
logger.info(f"Added new agent {agent.name} (ID: {agent_id}) to cloud registry")
|
|
348
|
+
else:
|
|
349
|
+
logger.info(f"Updated existing agent {agent.name} (ID: {agent_id}) in cloud registry")
|
|
350
|
+
|
|
351
|
+
return entry
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
async def get_agent_by_id(self, agent_id: str) -> Optional[UtilityAgentRegistryEntry]:
|
|
364
|
+
"""Get a specific agent by ID from cloud registry."""
|
|
365
|
+
doc = self.registry.find_one({"agent_id": agent_id})
|
|
366
|
+
if doc:
|
|
367
|
+
doc.pop("_id", None)
|
|
368
|
+
return UtilityAgentRegistryEntry(**doc)
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
async def update_health_status(self, agent_id: str, is_healthy: bool = True) -> bool:
|
|
372
|
+
"""Update health status of an agent in cloud registry."""
|
|
373
|
+
update = {
|
|
374
|
+
"$set": {
|
|
375
|
+
"last_health_check": get_now_in_utc(),
|
|
376
|
+
"is_active": is_healthy
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
result = self.registry.update_one(
|
|
381
|
+
{"agent_id": agent_id},
|
|
382
|
+
update
|
|
383
|
+
)
|
|
384
|
+
return result.modified_count > 0
|
|
385
|
+
|
|
386
|
+
async def update_agent_base_url(self, agent_id: str, new_base_url: str) -> bool:
|
|
387
|
+
"""Update the base URL for an agent.
|
|
388
|
+
|
|
389
|
+
This will also update the endpoints configuration with new derived URLs.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
agent_id: ID of the agent to update
|
|
393
|
+
new_base_url: New base URL for the agent
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
True if update was successful
|
|
397
|
+
"""
|
|
398
|
+
# Fetch the current agent to get streaming configuration
|
|
399
|
+
doc = self.registry.find_one({"agent_id": agent_id})
|
|
400
|
+
if not doc:
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
# Create new endpoints based on the new base URL
|
|
404
|
+
from ..utils.iatp_utils import create_iatp_endpoints
|
|
405
|
+
supports_streaming = doc.get("endpoints", {}).get("streaming_endpoint") is not None
|
|
406
|
+
new_endpoints = create_iatp_endpoints(new_base_url, supports_streaming)
|
|
407
|
+
|
|
408
|
+
result = self.registry.update_one(
|
|
409
|
+
{"agent_id": agent_id},
|
|
410
|
+
{"$set": {
|
|
411
|
+
"base_url": new_base_url,
|
|
412
|
+
"endpoints": new_endpoints.model_dump(mode='json'),
|
|
413
|
+
"updated_at": get_now_in_utc()
|
|
414
|
+
}}
|
|
415
|
+
)
|
|
416
|
+
return result.modified_count > 0
|
|
417
|
+
|
|
418
|
+
async def add_tags(self, agent_id: str, tags: List[str]) -> bool:
|
|
419
|
+
"""Add tags to an agent."""
|
|
420
|
+
result = self.registry.update_one(
|
|
421
|
+
{"agent_id": agent_id},
|
|
422
|
+
{"$addToSet": {"tags": {"$each": tags}}}
|
|
423
|
+
)
|
|
424
|
+
return result.modified_count > 0
|
|
425
|
+
|
|
426
|
+
async def remove_agent(self, agent_id: str) -> bool:
|
|
427
|
+
"""Remove an agent from the registry (soft delete by deactivating)."""
|
|
428
|
+
result = self.registry.update_one(
|
|
429
|
+
{"agent_id": agent_id},
|
|
430
|
+
{"$set": {"is_active": False, "deactivated_at": get_now_in_utc()}}
|
|
431
|
+
)
|
|
432
|
+
return result.modified_count > 0
|
|
433
|
+
|
|
434
|
+
async def update_utility_agent(self, agent_id: str, update_data: Dict[str, Any]) -> bool:
|
|
435
|
+
"""Update utility agent data in the registry.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
agent_id: The ID of the agent to update
|
|
439
|
+
update_data: Dictionary of fields to update
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
True if update was successful, False otherwise
|
|
443
|
+
"""
|
|
444
|
+
# Add updated_at timestamp
|
|
445
|
+
update_data["updated_at"] = get_now_in_utc()
|
|
446
|
+
|
|
447
|
+
result = self.registry.update_one(
|
|
448
|
+
{"agent_id": agent_id},
|
|
449
|
+
{"$set": update_data}
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if result.modified_count > 0:
|
|
453
|
+
logger.info(f"Updated utility agent {agent_id} with {len(update_data)} fields")
|
|
454
|
+
return True
|
|
455
|
+
else:
|
|
456
|
+
logger.warning(f"No utility agent found with ID {agent_id} or no changes made")
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
async def get_statistics(self) -> Dict[str, Any]:
|
|
460
|
+
"""Get registry statistics."""
|
|
461
|
+
total_agents = self.registry.count_documents({})
|
|
462
|
+
active_agents = self.registry.count_documents({"is_active": True})
|
|
463
|
+
|
|
464
|
+
# Get capability distribution
|
|
465
|
+
pipeline = [
|
|
466
|
+
{"$match": {"is_active": True}},
|
|
467
|
+
{"$unwind": "$capabilities"},
|
|
468
|
+
{"$group": {"_id": "$capabilities", "count": {"$sum": 1}}},
|
|
469
|
+
{"$sort": {"count": -1}},
|
|
470
|
+
{"$limit": 10}
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
capability_dist = list(self.registry.aggregate(pipeline))
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
"total_agents": total_agents,
|
|
477
|
+
"active_agents": active_agents,
|
|
478
|
+
"top_capabilities": [{"capability": item["_id"], "count": item["count"]} for item in capability_dist]
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
def close(self):
|
|
482
|
+
"""Close the MongoDB connection."""
|
|
483
|
+
self.client.close()
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class MCPServerRegistry:
|
|
487
|
+
"""Registry for MCP servers in cloud MongoDB (write operations only).
|
|
488
|
+
|
|
489
|
+
This class handles all write operations for MCP servers:
|
|
490
|
+
- Adding new MCP servers to the registry
|
|
491
|
+
- Updating server information
|
|
492
|
+
- Managing server lifecycle
|
|
493
|
+
|
|
494
|
+
For search and query operations, use iatp_search_api.py instead.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
def __init__(self, connection_string: Optional[str] = None, database_name: str = "iatp"):
|
|
498
|
+
"""Initialize MCP Server registry."""
|
|
499
|
+
if connection_string:
|
|
500
|
+
self.connection_string = connection_string
|
|
501
|
+
else:
|
|
502
|
+
# Try X.509 certificate authentication first
|
|
503
|
+
cert_file = os.getenv("MONGODB_X509_CERT_FILE")
|
|
504
|
+
if cert_file and os.path.exists(cert_file):
|
|
505
|
+
# For X.509 authentication, we need to extract the subject from the certificate
|
|
506
|
+
# to use as the username. MongoDB Atlas typically uses the full DN as username.
|
|
507
|
+
# The connection string format for X.509 is:
|
|
508
|
+
# mongodb+srv://cluster.mongodb.net/?authSource=$external&authMechanism=MONGODB-X509
|
|
509
|
+
# Extract just the cluster hostname without query parameters
|
|
510
|
+
cluster_host = CLUSTER_URI.split('?')[0]
|
|
511
|
+
self.connection_string = f"mongodb+srv://{cluster_host}?authSource=$external&authMechanism=MONGODB-X509&tls=true&tlsCertificateKeyFile={cert_file}"
|
|
512
|
+
logger.info(f"Using X.509 certificate authentication from {cert_file}")
|
|
513
|
+
else:
|
|
514
|
+
# Fallback to username/password authentication
|
|
515
|
+
user = os.getenv("MONGODB_USER")
|
|
516
|
+
password = os.getenv("MONGODB_PASSWORD")
|
|
517
|
+
if user and password:
|
|
518
|
+
self.connection_string = f"mongodb+srv://{user}:{password}@{CLUSTER_URI}"
|
|
519
|
+
logger.info("Using username/password authentication")
|
|
520
|
+
else:
|
|
521
|
+
# Try connection string as last resort
|
|
522
|
+
self.connection_string = os.getenv("MONGODB_CONNECTION_STRING")
|
|
523
|
+
if not self.connection_string:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
"MongoDB authentication required. Please provide either:\n"
|
|
526
|
+
"1. MONGODB_X509_CERT_FILE - Path to X.509 certificate file\n"
|
|
527
|
+
"2. MONGODB_USER and MONGODB_PASSWORD - Username and password\n"
|
|
528
|
+
"3. MONGODB_CONNECTION_STRING - Full connection string"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
self.client = _create_mongodb_client_with_retry(self.connection_string)
|
|
532
|
+
self.db: Database = self.client[database_name]
|
|
533
|
+
|
|
534
|
+
# Get environment-specific collection name
|
|
535
|
+
collection_names = get_collection_names()
|
|
536
|
+
self.collection: Collection = self.db[collection_names["mcp_server"]]
|
|
537
|
+
logger.info(f"Using collection: {collection_names['mcp_server']}")
|
|
538
|
+
|
|
539
|
+
# Ensure indexes
|
|
540
|
+
self._ensure_indexes()
|
|
541
|
+
|
|
542
|
+
def _ensure_indexes(self):
|
|
543
|
+
"""Ensure indexes exist."""
|
|
544
|
+
existing_indexes = [idx['name'] for idx in self.collection.list_indexes()]
|
|
545
|
+
|
|
546
|
+
# NOTE: Atlas Search and Vector Search indexes must be created through Atlas UI or API
|
|
547
|
+
# See atlas_search_indexes.json and ATLAS_SEARCH_SETUP.md for instructions
|
|
548
|
+
|
|
549
|
+
# Only create regular indexes (not text search)
|
|
550
|
+
if 'name_index' not in existing_indexes:
|
|
551
|
+
self.collection.create_index("name", unique=True, name='name_index')
|
|
552
|
+
|
|
553
|
+
async def add_mcp_server(
|
|
554
|
+
self,
|
|
555
|
+
name: str,
|
|
556
|
+
url: str,
|
|
557
|
+
description: str,
|
|
558
|
+
server_type: str = "streamable-http",
|
|
559
|
+
capabilities: List[str] = None,
|
|
560
|
+
metadata: Dict[str, Any] = None
|
|
561
|
+
) -> str:
|
|
562
|
+
"""Add an MCP server to the registry with retry logic."""
|
|
563
|
+
# Generate embeddings if enabled
|
|
564
|
+
description_embedding = None
|
|
565
|
+
capabilities_embedding = None
|
|
566
|
+
|
|
567
|
+
if os.getenv("ENABLE_EMBEDDINGS", "true").lower() == "true":
|
|
568
|
+
try:
|
|
569
|
+
embedding_service = get_embedding_service()
|
|
570
|
+
|
|
571
|
+
# Generate embedding for description
|
|
572
|
+
if description:
|
|
573
|
+
description_embedding = await embedding_service.generate_embedding(description)
|
|
574
|
+
|
|
575
|
+
# Generate embedding for capabilities (concatenated)
|
|
576
|
+
if capabilities:
|
|
577
|
+
capabilities_text = " ".join(capabilities)
|
|
578
|
+
capabilities_embedding = await embedding_service.generate_embedding(capabilities_text)
|
|
579
|
+
|
|
580
|
+
logger.info(f"Generated embeddings for MCP server {name}")
|
|
581
|
+
except Exception as e:
|
|
582
|
+
logger.warning(f"Failed to generate embeddings: {e}. Proceeding without embeddings.")
|
|
583
|
+
|
|
584
|
+
doc = {
|
|
585
|
+
"name": name,
|
|
586
|
+
"url": url,
|
|
587
|
+
"description": description,
|
|
588
|
+
"server_type": server_type,
|
|
589
|
+
"capabilities": capabilities or [],
|
|
590
|
+
"metadata": metadata or {},
|
|
591
|
+
"registered_at": get_now_in_utc(),
|
|
592
|
+
"is_active": True
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Add embeddings if available
|
|
596
|
+
if description_embedding:
|
|
597
|
+
doc["description_embedding"] = description_embedding
|
|
598
|
+
if capabilities_embedding:
|
|
599
|
+
doc["capabilities_embedding"] = capabilities_embedding
|
|
600
|
+
|
|
601
|
+
# Retry database operation with exponential backoff
|
|
602
|
+
max_retries = 3
|
|
603
|
+
for attempt in range(max_retries):
|
|
604
|
+
try:
|
|
605
|
+
# Upsert by name
|
|
606
|
+
result = self.collection.replace_one(
|
|
607
|
+
{"name": name},
|
|
608
|
+
doc,
|
|
609
|
+
upsert=True
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
if result.upserted_id:
|
|
613
|
+
logger.info(f"Added new MCP server {name} to registry")
|
|
614
|
+
else:
|
|
615
|
+
logger.info(f"Updated existing MCP server {name} in registry")
|
|
616
|
+
|
|
617
|
+
return str(result.upserted_id or result.matched_count)
|
|
618
|
+
|
|
619
|
+
except (ServerSelectionTimeoutError, NetworkTimeout, AutoReconnect, Exception) as e:
|
|
620
|
+
error_msg = str(e).lower()
|
|
621
|
+
is_retryable = any(term in error_msg for term in ['ssl', 'tls', 'handshake', 'timeout', 'network', 'connection'])
|
|
622
|
+
|
|
623
|
+
if attempt < max_retries - 1 and is_retryable:
|
|
624
|
+
delay = (2 ** attempt) + (attempt * 0.2) # Exponential backoff with jitter
|
|
625
|
+
logger.warning(f"Database operation attempt {attempt + 1} failed: {e}")
|
|
626
|
+
logger.info(f"Retrying in {delay:.1f} seconds...")
|
|
627
|
+
await asyncio.sleep(delay)
|
|
628
|
+
else:
|
|
629
|
+
logger.error(f"Failed to add MCP server {name} after {max_retries} attempts")
|
|
630
|
+
raise
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
async def get_mcp_server(self, name: str) -> Optional[Dict[str, Any]]:
|
|
641
|
+
"""Get an MCP server by name."""
|
|
642
|
+
doc = self.collection.find_one({"name": name})
|
|
643
|
+
if doc:
|
|
644
|
+
doc["_id"] = str(doc["_id"])
|
|
645
|
+
return doc
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
def close(self):
|
|
649
|
+
"""Close the connection."""
|
|
650
|
+
self.client.close()
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
if __name__ == "__main__":
|
|
654
|
+
import asyncio
|
|
655
|
+
import sys
|
|
656
|
+
import uuid
|
|
657
|
+
from dotenv import load_dotenv
|
|
658
|
+
|
|
659
|
+
# Load environment variables from .env file
|
|
660
|
+
load_dotenv()
|
|
661
|
+
|
|
662
|
+
# No additional imports needed
|
|
663
|
+
|
|
664
|
+
# Set up logging
|
|
665
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
666
|
+
|
|
667
|
+
async def test_registries():
|
|
668
|
+
"""Test both registries."""
|
|
669
|
+
print("\n=== MongoDB Registry Test ===")
|
|
670
|
+
print(f"Environment: {os.getenv('ENV', 'test')}")
|
|
671
|
+
collection_names = get_collection_names()
|
|
672
|
+
print(f"Collections: {collection_names}")
|
|
673
|
+
|
|
674
|
+
# Test required environment variables
|
|
675
|
+
if not os.getenv("MONGODB_READWRITE_CONNECTION_STRING"):
|
|
676
|
+
if not os.getenv("MONGODB_USER") or not os.getenv("MONGODB_PASSWORD"):
|
|
677
|
+
print("\nERROR: Please set MONGODB_READWRITE_CONNECTION_STRING or MONGODB_USER/PASSWORD environment variables")
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
# Test UtilityAgentRegistry
|
|
681
|
+
print("\n--- Testing UtilityAgentRegistry ---")
|
|
682
|
+
try:
|
|
683
|
+
agent_registry = UtilityAgentRegistry()
|
|
684
|
+
print(f"✓ Connected to MongoDB")
|
|
685
|
+
print(f"✓ Using database: {agent_registry.database_name}")
|
|
686
|
+
print(f"✓ Using collection: {collection_names['utility_agent']}")
|
|
687
|
+
|
|
688
|
+
# Create test agent
|
|
689
|
+
test_agent = UtilityAgent(
|
|
690
|
+
id=str(uuid.uuid4()),
|
|
691
|
+
name="Test Weather Agent",
|
|
692
|
+
description="A test agent for weather analysis",
|
|
693
|
+
mcp_server_id="test-mcp-server-id",
|
|
694
|
+
capabilities=["weather_current", "weather_forecast"],
|
|
695
|
+
tags=["weather", "test"],
|
|
696
|
+
metadata={
|
|
697
|
+
"test": True,
|
|
698
|
+
"created_by": "test_script"
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
# Create test agent with endpoints
|
|
703
|
+
from ..utils.iatp_utils import create_iatp_endpoints
|
|
704
|
+
test_agent.endpoints = create_iatp_endpoints("http://weather-agent:8100", supports_streaming=True)
|
|
705
|
+
|
|
706
|
+
# Add to registry
|
|
707
|
+
entry = await agent_registry.add_utility_agent(
|
|
708
|
+
test_agent,
|
|
709
|
+
tags=["weather", "test", "api"]
|
|
710
|
+
)
|
|
711
|
+
print(f"✓ Added test agent: {entry.name} (ID: {entry.agent_id})")
|
|
712
|
+
|
|
713
|
+
# Note: Query methods moved to iatp_search_api.py
|
|
714
|
+
print(f"✓ Agent added successfully - use iatp_search_api.py for queries")
|
|
715
|
+
|
|
716
|
+
# Get statistics
|
|
717
|
+
stats = await agent_registry.get_statistics()
|
|
718
|
+
print(f"✓ Registry statistics: {stats}")
|
|
719
|
+
|
|
720
|
+
# Update health status - use the returned entry's agent_id
|
|
721
|
+
updated = await agent_registry.update_health_status(entry.agent_id)
|
|
722
|
+
print(f"✓ Updated health status: {updated}")
|
|
723
|
+
|
|
724
|
+
agent_registry.close()
|
|
725
|
+
|
|
726
|
+
except Exception as e:
|
|
727
|
+
print(f"✗ UtilityAgentRegistry error: {e}")
|
|
728
|
+
import traceback
|
|
729
|
+
traceback.print_exc()
|
|
730
|
+
|
|
731
|
+
# Test MCPServerRegistry
|
|
732
|
+
print("\n--- Testing MCPServerRegistry ---")
|
|
733
|
+
try:
|
|
734
|
+
# Need to set MONGODB_URI for MCPServerRegistry
|
|
735
|
+
if not os.getenv("MONGODB_URI"):
|
|
736
|
+
os.environ["MONGODB_URI"] = f"mongodb+srv://{os.getenv('MONGODB_USER')}:{os.getenv('MONGODB_PASSWORD')}@{CLUSTER_URI}"
|
|
737
|
+
|
|
738
|
+
mcp_registry = MCPServerRegistry()
|
|
739
|
+
print(f"✓ Connected to MongoDB")
|
|
740
|
+
print(f"✓ Using collection: {collection_names['mcp_server']}")
|
|
741
|
+
|
|
742
|
+
# Add test MCP server
|
|
743
|
+
server_id = await mcp_registry.add_mcp_server(
|
|
744
|
+
name="test-weather-mcp",
|
|
745
|
+
url="http://weather-mcp:8080",
|
|
746
|
+
description="Test weather MCP server",
|
|
747
|
+
server_type="streamable-http",
|
|
748
|
+
capabilities=["weather", "forecast"],
|
|
749
|
+
metadata={"version": "1.0.0"}
|
|
750
|
+
)
|
|
751
|
+
print(f"✓ Added test MCP server: test-weather-mcp")
|
|
752
|
+
|
|
753
|
+
# Note: Query methods moved to iatp_search_api.py
|
|
754
|
+
print(f"✓ MCP server added successfully - use iatp_search_api.py for queries")
|
|
755
|
+
|
|
756
|
+
# Get specific server
|
|
757
|
+
server = await mcp_registry.get_mcp_server("test-weather-mcp")
|
|
758
|
+
if server:
|
|
759
|
+
print(f"✓ Retrieved MCP server: {server['name']}")
|
|
760
|
+
|
|
761
|
+
mcp_registry.close()
|
|
762
|
+
|
|
763
|
+
except Exception as e:
|
|
764
|
+
print(f"✗ MCPServerRegistry error: {e}")
|
|
765
|
+
import traceback
|
|
766
|
+
traceback.print_exc()
|
|
767
|
+
|
|
768
|
+
print("\n=== Test Complete ===")
|
|
769
|
+
|
|
770
|
+
# Run the test
|
|
771
|
+
asyncio.run(test_registries())
|