traia-iatp 0.1.2__py3-none-any.whl → 0.1.67__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.
- traia_iatp/__init__.py +105 -8
- traia_iatp/cli/main.py +85 -1
- traia_iatp/client/__init__.py +28 -3
- traia_iatp/client/crewai_a2a_tools.py +32 -12
- traia_iatp/client/d402_a2a_client.py +348 -0
- traia_iatp/contracts/__init__.py +11 -0
- traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
- traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
- traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
- traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +369 -0
- traia_iatp/core/models.py +17 -3
- traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
- traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
- traia_iatp/d402/README.md +489 -0
- traia_iatp/d402/__init__.py +54 -0
- traia_iatp/d402/asgi_wrapper.py +469 -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 +266 -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 +481 -0
- traia_iatp/d402/mcp_middleware.py +296 -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 +126 -0
- traia_iatp/d402/payment_signing.py +183 -0
- traia_iatp/d402/price_builder.py +164 -0
- traia_iatp/d402/servers/__init__.py +61 -0
- traia_iatp/d402/servers/base.py +139 -0
- traia_iatp/d402/servers/example_general_server.py +140 -0
- traia_iatp/d402/servers/fastapi.py +253 -0
- traia_iatp/d402/servers/mcp.py +304 -0
- traia_iatp/d402/servers/starlette.py +878 -0
- traia_iatp/d402/starlette_middleware.py +529 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
- traia_iatp/mcp/__init__.py +3 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
- traia_iatp/mcp/mcp_agent_template.py +78 -13
- traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
- traia_iatp/mcp/templates/README.md.j2 +104 -8
- traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
- traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
- traia_iatp/mcp/templates/env.example.j2 +60 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
- traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
- traia_iatp/mcp/templates/server.py.j2 +174 -197
- traia_iatp/mcp/traia_mcp_adapter.py +182 -20
- traia_iatp/registry/__init__.py +47 -12
- traia_iatp/registry/atlas_search_indexes.json +108 -54
- traia_iatp/registry/iatp_search_api.py +169 -39
- traia_iatp/registry/mongodb_registry.py +241 -69
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
- traia_iatp/registry/readmes/README.md +3 -3
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/a2a_server.py +22 -7
- traia_iatp/server/iatp_server_template_generator.py +23 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +23 -1
- traia_iatp/server/templates/README.md +2 -2
- traia_iatp/server/templates/README.md.j2 +5 -5
- traia_iatp/server/templates/__main__.py.j2 +374 -66
- traia_iatp/server/templates/agent.py.j2 +12 -11
- traia_iatp/server/templates/agent_config.json.j2 +3 -3
- traia_iatp/server/templates/agent_executor.py.j2 +45 -27
- traia_iatp/server/templates/env.example.j2 +32 -4
- traia_iatp/server/templates/gitignore.j2 +7 -0
- traia_iatp/server/templates/pyproject.toml.j2 +13 -12
- traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
- traia_iatp/server/templates/server.py.j2 +197 -10
- traia_iatp/special_agencies/registry_search_agency.py +1 -1
- traia_iatp/utils/iatp_utils.py +6 -6
- traia_iatp-0.1.67.dist-info/METADATA +320 -0
- traia_iatp-0.1.67.dist-info/RECORD +117 -0
- traia_iatp-0.1.2.dist-info/METADATA +0 -414
- traia_iatp-0.1.2.dist-info/RECORD +0 -72
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/top_level.txt +0 -0
|
@@ -24,14 +24,22 @@ from a2a.types import AgentCard, AgentSkill, AgentCapabilities
|
|
|
24
24
|
from a2a.server.tasks import InMemoryTaskStore
|
|
25
25
|
from a2a.server.request_handlers import DefaultRequestHandler
|
|
26
26
|
from a2a.server.events.event_queue import EventQueue
|
|
27
|
-
|
|
28
|
-
from
|
|
29
|
-
from starlette.responses import StreamingResponse
|
|
27
|
+
import uvicorn
|
|
28
|
+
from starlette.responses import StreamingResponse, JSONResponse
|
|
30
29
|
from starlette.requests import Request
|
|
31
30
|
from starlette.routing import Route
|
|
32
31
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
32
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
33
33
|
from starlette.responses import Response
|
|
34
34
|
|
|
35
|
+
# Import D402 payment middleware and helpers (generalized version)
|
|
36
|
+
from traia_iatp.d402.servers import D402PaymentMiddleware, build_payment_config
|
|
37
|
+
from traia_iatp.d402.asgi_wrapper import D402ASGIWrapper
|
|
38
|
+
from traia_iatp.d402 import D402PriceBuilder
|
|
39
|
+
|
|
40
|
+
# Import httpx for MCP server verification
|
|
41
|
+
import httpx
|
|
42
|
+
|
|
35
43
|
# Import AgentOps for monitoring and observability
|
|
36
44
|
try:
|
|
37
45
|
import agentops
|
|
@@ -48,13 +56,22 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
48
56
|
from traia_iatp.mcp import MCPServerConfig
|
|
49
57
|
from .agent_executor import {{ class_name }}AgentExecutor
|
|
50
58
|
|
|
51
|
-
# Configure logging
|
|
59
|
+
# Configure logging FIRST (before any logger usage)
|
|
52
60
|
logging.basicConfig(
|
|
53
61
|
level=logging.INFO,
|
|
54
62
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
55
63
|
)
|
|
56
64
|
logger = logging.getLogger(__name__)
|
|
57
65
|
|
|
66
|
+
# D402 payment support is configured via environment variables and handled by middleware
|
|
67
|
+
# The D402PaymentMiddleware is added to the Starlette app based on .env configuration
|
|
68
|
+
# No additional imports needed here - middleware handles all D402 logic
|
|
69
|
+
D402_AVAILABLE = os.getenv("D402_ENABLED", "false").lower() == "true"
|
|
70
|
+
if D402_AVAILABLE:
|
|
71
|
+
logger.info("✅ D402 payment support enabled (configured via environment)")
|
|
72
|
+
else:
|
|
73
|
+
logger.info("ℹ️ D402 payment support disabled (set D402_ENABLED=true to enable)")
|
|
74
|
+
|
|
58
75
|
# Enable debug logging for HTTP/2 and connection events if requested
|
|
59
76
|
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
60
77
|
logging.getLogger("hypercorn.access").setLevel(logging.DEBUG)
|
|
@@ -109,6 +126,119 @@ class ProtocolLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
109
126
|
return response
|
|
110
127
|
|
|
111
128
|
|
|
129
|
+
# Global state for MCP connection tracking
|
|
130
|
+
mcp_connection_status = {
|
|
131
|
+
"verified": False,
|
|
132
|
+
"last_checked": None,
|
|
133
|
+
"error": None
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async def verify_mcp_connection(mcp_url: str, mcp_name: str) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Verify connection to the MCP server by testing endpoints.
|
|
139
|
+
Returns True if connection is working, False otherwise.
|
|
140
|
+
"""
|
|
141
|
+
global mcp_connection_status
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
logger.info(f"🔍 Verifying MCP server connection to: {mcp_url}")
|
|
145
|
+
|
|
146
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
147
|
+
# Try to test the MCP server is reachable
|
|
148
|
+
# First try .well-known/mcp.json (if available)
|
|
149
|
+
card_url = f"{mcp_url}/.well-known/mcp.json"
|
|
150
|
+
logger.info(f" Trying MCP card at: {card_url}")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
response = await client.get(card_url)
|
|
154
|
+
response.raise_for_status()
|
|
155
|
+
|
|
156
|
+
mcp_card = response.json()
|
|
157
|
+
logger.info(f" ✅ MCP card fetched successfully")
|
|
158
|
+
logger.info(f" MCP Server: {mcp_card.get('name', 'Unknown')}")
|
|
159
|
+
logger.info(f" Capabilities: {len(mcp_card.get('capabilities', []))} tools")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
# Well-known endpoint not available - that's OK for some MCP servers
|
|
162
|
+
logger.info(f" ℹ️ MCP card not available (some servers don't expose it): {e}")
|
|
163
|
+
|
|
164
|
+
# Test the MCP endpoint properly (like the tests do)
|
|
165
|
+
logger.info(f" Testing MCP JSON-RPC endpoint: {mcp_url}")
|
|
166
|
+
|
|
167
|
+
# Step 1: Initialize MCP session (required before tools/list)
|
|
168
|
+
init_request = {
|
|
169
|
+
"jsonrpc": "2.0",
|
|
170
|
+
"method": "initialize",
|
|
171
|
+
"id": "verify-init",
|
|
172
|
+
"params": {
|
|
173
|
+
"protocolVersion": "2024-11-05",
|
|
174
|
+
"capabilities": {},
|
|
175
|
+
"clientInfo": {"name": "utility-agent-verifier", "version": "1.0"}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
init_response = await client.post(
|
|
180
|
+
mcp_url,
|
|
181
|
+
json=init_request,
|
|
182
|
+
headers={
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"Accept": "application/json, text/event-stream"
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
init_response.raise_for_status()
|
|
188
|
+
|
|
189
|
+
session_id = init_response.headers.get("mcp-session-id")
|
|
190
|
+
if session_id:
|
|
191
|
+
logger.info(f" ✅ MCP session initialized: {session_id[:20]}...")
|
|
192
|
+
else:
|
|
193
|
+
logger.warning(f" ⚠️ No session ID returned (may be OK for some servers)")
|
|
194
|
+
|
|
195
|
+
# Step 2: Test tools/list with session
|
|
196
|
+
list_request = {
|
|
197
|
+
"jsonrpc": "2.0",
|
|
198
|
+
"method": "tools/list",
|
|
199
|
+
"id": "verify-list"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
list_headers = {
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
"Accept": "application/json, text/event-stream"
|
|
205
|
+
}
|
|
206
|
+
if session_id:
|
|
207
|
+
list_headers["mcp-session-id"] = session_id
|
|
208
|
+
|
|
209
|
+
list_response = await client.post(
|
|
210
|
+
mcp_url,
|
|
211
|
+
json=list_request,
|
|
212
|
+
headers=list_headers
|
|
213
|
+
)
|
|
214
|
+
list_response.raise_for_status()
|
|
215
|
+
|
|
216
|
+
logger.info(f" ✅ MCP server responding correctly (HTTP {list_response.status_code})")
|
|
217
|
+
|
|
218
|
+
# Update connection status
|
|
219
|
+
mcp_connection_status["verified"] = True
|
|
220
|
+
mcp_connection_status["last_checked"] = datetime.now()
|
|
221
|
+
mcp_connection_status["error"] = None
|
|
222
|
+
|
|
223
|
+
logger.info(f"✅ MCP server connection verified successfully!")
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
except httpx.HTTPError as e:
|
|
227
|
+
error_msg = f"HTTP error connecting to MCP server: {e}"
|
|
228
|
+
logger.error(f"❌ {error_msg}")
|
|
229
|
+
mcp_connection_status["verified"] = False
|
|
230
|
+
mcp_connection_status["last_checked"] = datetime.now()
|
|
231
|
+
mcp_connection_status["error"] = error_msg
|
|
232
|
+
return False
|
|
233
|
+
except Exception as e:
|
|
234
|
+
error_msg = f"Unexpected error verifying MCP connection: {e}"
|
|
235
|
+
logger.error(f"❌ {error_msg}")
|
|
236
|
+
mcp_connection_status["verified"] = False
|
|
237
|
+
mcp_connection_status["last_checked"] = datetime.now()
|
|
238
|
+
mcp_connection_status["error"] = error_msg
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
|
|
112
242
|
class StreamingRequestHandler(DefaultRequestHandler):
|
|
113
243
|
"""Extended request handler with SSE streaming support."""
|
|
114
244
|
|
|
@@ -239,6 +369,48 @@ def create_app():
|
|
|
239
369
|
supports_streaming = mcp_data.get("server_type") == "streamable-http" or \
|
|
240
370
|
"stream" in mcp_data.get("capabilities", [])
|
|
241
371
|
|
|
372
|
+
# D402 Configuration (if enabled) - for A2A endpoint protection
|
|
373
|
+
d402_enabled = D402_AVAILABLE and os.getenv("D402_ENABLED", "false").lower() == "true"
|
|
374
|
+
d402_token_amount = None
|
|
375
|
+
|
|
376
|
+
if d402_enabled:
|
|
377
|
+
logger.info("✅ D402 payments enabled")
|
|
378
|
+
|
|
379
|
+
contract_address = os.getenv("UTILITY_AGENT_CONTRACT_ADDRESS")
|
|
380
|
+
token_address = os.getenv("D402_TOKEN_ADDRESS")
|
|
381
|
+
token_symbol = os.getenv("D402_TOKEN_SYMBOL", "USDC")
|
|
382
|
+
token_decimals = int(os.getenv("D402_TOKEN_DECIMALS", "6"))
|
|
383
|
+
network = os.getenv("D402_NETWORK", "sepolia")
|
|
384
|
+
facilitator_url = os.getenv("D402_FACILITATOR_URL", "http://localhost:7070")
|
|
385
|
+
operator_key = os.getenv("UTILITY_AGENT_OPERATOR_PRIVATE_KEY")
|
|
386
|
+
testing_mode = os.getenv("D402_TESTING_MODE", "false").lower() == "true"
|
|
387
|
+
|
|
388
|
+
if not contract_address:
|
|
389
|
+
logger.error("❌ D402 enabled but UTILITY_AGENT_CONTRACT_ADDRESS not set!")
|
|
390
|
+
d402_enabled = False
|
|
391
|
+
elif not token_address:
|
|
392
|
+
logger.error("❌ D402 enabled but D402_TOKEN_ADDRESS not set!")
|
|
393
|
+
d402_enabled = False
|
|
394
|
+
else:
|
|
395
|
+
# Use D402PriceBuilder for clean price creation
|
|
396
|
+
usd_price = float(os.getenv("D402_PRICE_USD", "0.01"))
|
|
397
|
+
|
|
398
|
+
price_builder = D402PriceBuilder(
|
|
399
|
+
token_address=token_address,
|
|
400
|
+
token_decimals=token_decimals,
|
|
401
|
+
network=network,
|
|
402
|
+
token_symbol=token_symbol
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
d402_token_amount = price_builder.create_price(usd_price)
|
|
406
|
+
|
|
407
|
+
logger.info(f"💰 D402 Price: ${usd_price} USD ({d402_token_amount.amount} wei with {token_decimals} decimals)")
|
|
408
|
+
logger.info(f"📍 Pay to: {contract_address}")
|
|
409
|
+
logger.info(f"🌐 Network: {network}")
|
|
410
|
+
logger.info(f"🪙 Token: {token_symbol} ({token_address})")
|
|
411
|
+
logger.info(f"🔗 Facilitator: {facilitator_url}")
|
|
412
|
+
logger.info(f"🧪 Testing mode: {testing_mode}")
|
|
413
|
+
|
|
242
414
|
# Create agent skills based on MCP capabilities
|
|
243
415
|
skills = []
|
|
244
416
|
|
|
@@ -294,17 +466,30 @@ def create_app():
|
|
|
294
466
|
# Authentication can be added here if needed in the future
|
|
295
467
|
# Currently the A2A protocol handles authentication at a different layer
|
|
296
468
|
|
|
297
|
-
# Create agent card
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
469
|
+
# Create agent card with proper URL
|
|
470
|
+
# For Cloud Run: use PUBLIC_URL environment variable
|
|
471
|
+
# For local: use http://0.0.0.0:{port}/a2a
|
|
472
|
+
public_url = os.environ.get("PUBLIC_URL") or os.environ.get("SERVICE_URL")
|
|
473
|
+
if public_url:
|
|
474
|
+
# Cloud Run - use public URL with /a2a endpoint
|
|
475
|
+
agent_url = f"{public_url}/a2a"
|
|
476
|
+
else:
|
|
477
|
+
# Local - use 0.0.0.0:{port}
|
|
478
|
+
agent_url = f"http://0.0.0.0:{os.environ.get('PORT', 8000)}/a2a"
|
|
479
|
+
|
|
480
|
+
agent_card_dict = {
|
|
481
|
+
"name": "{{ agent_id }}",
|
|
482
|
+
"description": "{{ agent_description }}",
|
|
483
|
+
"url": agent_url,
|
|
484
|
+
"version": "{{ agent_version }}",
|
|
485
|
+
"capabilities": capabilities.model_dump(),
|
|
486
|
+
"skills": [s.model_dump() for s in skills],
|
|
487
|
+
"defaultInputModes": ["text", "text/plain"],
|
|
488
|
+
"defaultOutputModes": ["text", "text/plain", "text/event-stream"] if supports_streaming else ["text", "text/plain"]
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# D402 payment info is automatically added to agent card by middleware
|
|
492
|
+
agent_card = AgentCard(**agent_card_dict)
|
|
308
493
|
|
|
309
494
|
# Create executor with MCP config
|
|
310
495
|
executor = {{ class_name }}AgentExecutor(mcp_config, supports_streaming=supports_streaming)
|
|
@@ -322,8 +507,74 @@ def create_app():
|
|
|
322
507
|
http_handler=request_handler
|
|
323
508
|
)
|
|
324
509
|
|
|
325
|
-
# Build the Starlette app
|
|
326
|
-
|
|
510
|
+
# Build the Starlette app with A2A JSON-RPC endpoint at /a2a
|
|
511
|
+
# Add D402 middleware using Starlette's raw ASGI wrapper if enabled
|
|
512
|
+
if d402_enabled and d402_token_amount:
|
|
513
|
+
logger.info("✅ Configuring D402 payment middleware for A2A endpoints")
|
|
514
|
+
|
|
515
|
+
# Get D402 configuration from environment
|
|
516
|
+
contract_address = os.getenv("UTILITY_AGENT_CONTRACT_ADDRESS")
|
|
517
|
+
facilitator_url = os.getenv("D402_FACILITATOR_URL", "http://localhost:7070")
|
|
518
|
+
operator_key = os.getenv("UTILITY_AGENT_OPERATOR_PRIVATE_KEY")
|
|
519
|
+
testing_mode = os.getenv("D402_TESTING_MODE", "false").lower() == "true"
|
|
520
|
+
|
|
521
|
+
# Build payment configs for A2A endpoint
|
|
522
|
+
endpoint_payment_configs = {
|
|
523
|
+
"/a2a": build_payment_config(
|
|
524
|
+
price=d402_token_amount,
|
|
525
|
+
server_address=contract_address,
|
|
526
|
+
description="{{ agent_description }}"
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
logger.info(f" 💳 Middleware configured for {len(endpoint_payment_configs)} endpoint(s)")
|
|
531
|
+
logger.info(f" 🔒 Protected paths: {list(endpoint_payment_configs.keys())}")
|
|
532
|
+
logger.info(f" 🔐 Requires auth: False (payment-only)")
|
|
533
|
+
logger.info(f" 🧪 Testing mode: {testing_mode}")
|
|
534
|
+
|
|
535
|
+
# Build Starlette app first (without middleware)
|
|
536
|
+
starlette_app = app.build(rpc_url='/a2a')
|
|
537
|
+
|
|
538
|
+
# Add CORS middleware to allow all origins
|
|
539
|
+
starlette_app.add_middleware(
|
|
540
|
+
CORSMiddleware,
|
|
541
|
+
allow_origins=["*"], # Allow all origins
|
|
542
|
+
allow_credentials=True,
|
|
543
|
+
allow_methods=["*"], # Allow all methods
|
|
544
|
+
allow_headers=["*"], # Allow all headers
|
|
545
|
+
)
|
|
546
|
+
logger.info("✅ Added CORS middleware (allow all origins)")
|
|
547
|
+
|
|
548
|
+
# Wrap with raw ASGI wrapper (bypasses Starlette middleware issues)
|
|
549
|
+
# This intercepts at the lowest ASGI level before any framework processing
|
|
550
|
+
d402_wrapped_app = D402ASGIWrapper(
|
|
551
|
+
app=starlette_app,
|
|
552
|
+
server_address=contract_address,
|
|
553
|
+
endpoint_payment_configs=endpoint_payment_configs,
|
|
554
|
+
requires_auth=False,
|
|
555
|
+
internal_api_key=None,
|
|
556
|
+
testing_mode=testing_mode,
|
|
557
|
+
facilitator_url=facilitator_url,
|
|
558
|
+
facilitator_api_key=os.getenv("D402_FACILITATOR_API_KEY"),
|
|
559
|
+
server_name="{{ agent_id }}"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
logger.info(" ✅ D402 raw ASGI wrapper applied (guaranteed interception)")
|
|
563
|
+
else:
|
|
564
|
+
# No D402 - just build normally
|
|
565
|
+
starlette_app = app.build(rpc_url='/a2a')
|
|
566
|
+
|
|
567
|
+
# Add CORS middleware even without D402
|
|
568
|
+
starlette_app.add_middleware(
|
|
569
|
+
CORSMiddleware,
|
|
570
|
+
allow_origins=["*"], # Allow all origins
|
|
571
|
+
allow_credentials=True,
|
|
572
|
+
allow_methods=["*"], # Allow all methods
|
|
573
|
+
allow_headers=["*"], # Allow all headers
|
|
574
|
+
)
|
|
575
|
+
logger.info("✅ Added CORS middleware (allow all origins)")
|
|
576
|
+
|
|
577
|
+
d402_wrapped_app = None
|
|
327
578
|
|
|
328
579
|
# Add protocol logging middleware if debug mode is enabled
|
|
329
580
|
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
@@ -360,6 +611,54 @@ def create_app():
|
|
|
360
611
|
Route("/a2a/tasks/resubscribe", handle_resubscribe_endpoint, methods=["POST"])
|
|
361
612
|
)
|
|
362
613
|
|
|
614
|
+
# Add health endpoint with MCP connection verification
|
|
615
|
+
async def health_endpoint(request: Request):
|
|
616
|
+
"""Health check endpoint that verifies MCP connection every 10 minutes."""
|
|
617
|
+
global mcp_connection_status
|
|
618
|
+
|
|
619
|
+
# Check if we need to reverify (more than 10 minutes since last check)
|
|
620
|
+
now = datetime.now()
|
|
621
|
+
should_reverify = (
|
|
622
|
+
mcp_connection_status["last_checked"] is None or
|
|
623
|
+
(now - mcp_connection_status["last_checked"]).total_seconds() > 600 # 10 minutes
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if should_reverify:
|
|
627
|
+
logger.info("⏰ 10 minutes elapsed since last MCP verification, rechecking...")
|
|
628
|
+
await verify_mcp_connection(mcp_config.url, mcp_config.name)
|
|
629
|
+
|
|
630
|
+
# Return health status
|
|
631
|
+
if mcp_connection_status["verified"]:
|
|
632
|
+
return JSONResponse({
|
|
633
|
+
"status": "healthy",
|
|
634
|
+
"mcp_connection": "verified",
|
|
635
|
+
"mcp_server": mcp_config.name,
|
|
636
|
+
"mcp_url": mcp_config.url,
|
|
637
|
+
"last_verified": mcp_connection_status["last_checked"].isoformat() if mcp_connection_status["last_checked"] else None
|
|
638
|
+
})
|
|
639
|
+
else:
|
|
640
|
+
return JSONResponse(
|
|
641
|
+
{
|
|
642
|
+
"status": "unhealthy",
|
|
643
|
+
"mcp_connection": "failed",
|
|
644
|
+
"mcp_server": mcp_config.name,
|
|
645
|
+
"mcp_url": mcp_config.url,
|
|
646
|
+
"error": mcp_connection_status["error"],
|
|
647
|
+
"last_checked": mcp_connection_status["last_checked"].isoformat() if mcp_connection_status["last_checked"] else None
|
|
648
|
+
},
|
|
649
|
+
status_code=503
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
starlette_app.routes.append(
|
|
653
|
+
Route("/health", health_endpoint, methods=["GET"])
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Return the D402-wrapped app if D402 is enabled, otherwise return the unwrapped app
|
|
657
|
+
if d402_wrapped_app:
|
|
658
|
+
logger.info(f"🔒 Returning D402-wrapped app (type: {type(d402_wrapped_app).__name__})")
|
|
659
|
+
return d402_wrapped_app
|
|
660
|
+
else:
|
|
661
|
+
logger.info(f"⚠️ Returning unwrapped app (D402 disabled, type: {type(starlette_app).__name__})")
|
|
363
662
|
return starlette_app
|
|
364
663
|
|
|
365
664
|
|
|
@@ -391,8 +690,38 @@ def generate_self_signed_cert(cert_path: str = "cert.pem", key_path: str = "key.
|
|
|
391
690
|
raise
|
|
392
691
|
|
|
393
692
|
|
|
394
|
-
async def
|
|
395
|
-
"""
|
|
693
|
+
async def verify_mcp_before_start():
|
|
694
|
+
"""Verify MCP connection before starting server."""
|
|
695
|
+
# Get MCP configuration
|
|
696
|
+
mcp_url = os.getenv("MCP_SERVER_URL")
|
|
697
|
+
mcp_name = "{{ mcp_server_name }}"
|
|
698
|
+
|
|
699
|
+
if not mcp_url:
|
|
700
|
+
logger.error("MCP_SERVER_URL not set!")
|
|
701
|
+
return False
|
|
702
|
+
|
|
703
|
+
logger.info("="*80)
|
|
704
|
+
logger.info("🔍 Verifying MCP server connection before startup...")
|
|
705
|
+
logger.info("="*80)
|
|
706
|
+
|
|
707
|
+
mcp_ok = await verify_mcp_connection(mcp_url, mcp_name)
|
|
708
|
+
|
|
709
|
+
if not mcp_ok:
|
|
710
|
+
logger.error("="*80)
|
|
711
|
+
logger.error("❌ STARTUP FAILED: Cannot connect to MCP server!")
|
|
712
|
+
logger.error(" The utility agent will NOT start without MCP server access.")
|
|
713
|
+
logger.error(" Please check the MCP server is running and accessible.")
|
|
714
|
+
logger.error("="*80)
|
|
715
|
+
return False
|
|
716
|
+
|
|
717
|
+
logger.info("="*80)
|
|
718
|
+
logger.info("✅ MCP server verification complete - proceeding with startup")
|
|
719
|
+
logger.info("="*80)
|
|
720
|
+
return True
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def main():
|
|
724
|
+
"""Main function to start the A2A server."""
|
|
396
725
|
|
|
397
726
|
# Initialize AgentOps for monitoring and observability
|
|
398
727
|
agentops_session_id = None
|
|
@@ -423,65 +752,37 @@ async def main():
|
|
|
423
752
|
host = os.environ.get("HOST", "0.0.0.0")
|
|
424
753
|
port = int(os.environ.get("PORT", 8000))
|
|
425
754
|
|
|
755
|
+
# MCP was already verified by verify_mcp_before_start()
|
|
426
756
|
# Create the application
|
|
427
757
|
app = create_app()
|
|
428
758
|
|
|
429
|
-
# Configure Hypercorn for HTTP/2 support
|
|
430
|
-
config = Config()
|
|
431
|
-
config.bind = [f"{host}:{port}"]
|
|
432
|
-
config.alpn_protocols = ["h2", "http/1.1"] # Support both HTTP/2 and HTTP/1.1
|
|
433
|
-
|
|
434
|
-
# Enable access logging if debug mode is on
|
|
435
|
-
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
436
|
-
config.accesslog = "-" # Log to stdout
|
|
437
|
-
config.errorlog = "-" # Log errors to stdout
|
|
438
|
-
config.loglevel = "DEBUG"
|
|
439
|
-
logger.info("Hypercorn access logging enabled")
|
|
440
|
-
|
|
441
|
-
# Enable HTTP/2
|
|
442
|
-
config.h2_max_concurrent_streams = 100
|
|
443
|
-
config.h2_max_header_list_size = 8192
|
|
444
|
-
config.h2_max_inbound_frame_size = 16384
|
|
445
|
-
config.h2_initial_connection_window_size = 65536
|
|
446
|
-
|
|
447
|
-
# Connection settings for high performance
|
|
448
|
-
config.keep_alive_timeout = 300 # 5 minutes
|
|
449
|
-
config.max_requests = 10000 # Max requests per connection
|
|
450
|
-
config.max_requests_jitter = 1000 # Add jitter to prevent thundering herd
|
|
451
|
-
|
|
452
|
-
# SSL/TLS configuration for production (optional)
|
|
453
|
-
if os.environ.get("USE_TLS", "false").lower() == "true":
|
|
454
|
-
cert_path = os.environ.get("TLS_CERT_PATH", "cert.pem")
|
|
455
|
-
key_path = os.environ.get("TLS_KEY_PATH", "key.pem")
|
|
456
|
-
|
|
457
|
-
# Generate self-signed certificates for local development if needed
|
|
458
|
-
if os.environ.get("GENERATE_CERTS", "true").lower() == "true":
|
|
459
|
-
generate_self_signed_cert(cert_path, key_path)
|
|
460
|
-
|
|
461
|
-
if Path(cert_path).exists() and Path(key_path).exists():
|
|
462
|
-
config.certfile = cert_path
|
|
463
|
-
config.keyfile = key_path
|
|
464
|
-
config.alpn_protocols = ["h2", "http/1.1"]
|
|
465
|
-
logger.info("TLS enabled with HTTP/2 support")
|
|
466
|
-
logger.info("Using HTTPS - connect to https://localhost:8000")
|
|
467
|
-
else:
|
|
468
|
-
logger.warning("TLS requested but certificates not found")
|
|
469
|
-
|
|
470
759
|
# Log startup information
|
|
471
|
-
logger.info(f"Starting {{ agent_name }} A2A Server
|
|
760
|
+
logger.info(f"Starting {{ agent_name }} A2A Server")
|
|
472
761
|
logger.info(f"MCP Server: {{ mcp_server_name }}")
|
|
473
762
|
logger.info(f"Listening on {host}:{port}")
|
|
474
|
-
logger.info(f"Agent Card available at: http://{host}:{port}/.well-known/agent.json")
|
|
763
|
+
logger.info(f"Agent Card available at: http://{host}:{port}/.well-known/agent-card.json")
|
|
764
|
+
logger.info(f"A2A Endpoint: http://{host}:{port}/a2a")
|
|
475
765
|
logger.info(f"SSE endpoints available at: /a2a/tasks/subscribe and /a2a/tasks/resubscribe")
|
|
476
|
-
logger.info(f"HTTP/2 multiplexing enabled with max {config.h2_max_concurrent_streams} concurrent streams")
|
|
477
766
|
|
|
478
767
|
if agentops_session_id:
|
|
479
768
|
logger.info(f"📊 AgentOps Session ID: {agentops_session_id}")
|
|
480
769
|
logger.info("📊 Monitor agent performance at: https://app.agentops.ai")
|
|
481
770
|
|
|
771
|
+
# Configure Uvicorn (same as MCP servers for D402 compatibility)
|
|
772
|
+
uvicorn_config = {
|
|
773
|
+
"host": host,
|
|
774
|
+
"port": port,
|
|
775
|
+
"log_level": "info"
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
# Enable access logging if debug mode is on
|
|
779
|
+
if os.environ.get("DEBUG_PROTOCOL", "false").lower() == "true":
|
|
780
|
+
uvicorn_config["log_level"] = "debug"
|
|
781
|
+
logger.info("Debug logging enabled")
|
|
782
|
+
|
|
482
783
|
try:
|
|
483
|
-
# Run the server with
|
|
484
|
-
|
|
784
|
+
# Run the server with Uvicorn (for D402 middleware compatibility)
|
|
785
|
+
uvicorn.run(app, **uvicorn_config)
|
|
485
786
|
except KeyboardInterrupt:
|
|
486
787
|
logger.info("🛑 Server shutdown requested")
|
|
487
788
|
if AGENTOPS_AVAILABLE and agentops_session_id:
|
|
@@ -502,5 +803,12 @@ async def main():
|
|
|
502
803
|
|
|
503
804
|
|
|
504
805
|
if __name__ == "__main__":
|
|
806
|
+
# Run async MCP verification first
|
|
505
807
|
import asyncio
|
|
506
|
-
asyncio.run(
|
|
808
|
+
mcp_verified = asyncio.run(verify_mcp_before_start())
|
|
809
|
+
|
|
810
|
+
if not mcp_verified:
|
|
811
|
+
sys.exit(1)
|
|
812
|
+
|
|
813
|
+
# Then run synchronous main (Uvicorn handles its own event loop)
|
|
814
|
+
main()
|
|
@@ -6,6 +6,7 @@ Auto-generated for the {{ agent_name }} utility agent.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
import datetime
|
|
9
10
|
from typing import List, Dict, Any, Optional
|
|
10
11
|
from crewai import Agent, Task, Crew, LLM
|
|
11
12
|
import os
|
|
@@ -14,19 +15,18 @@ import os
|
|
|
14
15
|
from traia_iatp.mcp import MCPServerConfig, MCPAgentBuilder, run_with_mcp_tools, MCPServerInfo
|
|
15
16
|
|
|
16
17
|
# Import AgentOps for operation tracking
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def operation(func):
|
|
26
|
-
return func
|
|
18
|
+
import agentops
|
|
19
|
+
|
|
20
|
+
DEFAULT_LLM = LLM(
|
|
21
|
+
model=os.getenv("LLM_MODEL", "gpt-4.1-nano"), # Using environment variable with fallback
|
|
22
|
+
temperature=float(os.getenv("LLM_MODEL_TEMPERATURE", "0.1")),
|
|
23
|
+
api_key=os.getenv("OPENAI_API_KEY")
|
|
24
|
+
)
|
|
25
|
+
current_time = datetime.datetime.utcnow()
|
|
27
26
|
|
|
28
27
|
logger = logging.getLogger(__name__)
|
|
29
28
|
|
|
29
|
+
logger.info(f"Current LLM model used: {os.getenv("LLM_MODEL", "gpt-4.1-nano")}")
|
|
30
30
|
|
|
31
31
|
class {{ class_name }}Agent:
|
|
32
32
|
"""{{ agent_name }} agent that processes requests using {{ mcp_server_name }}."""
|
|
@@ -61,10 +61,11 @@ class {{ class_name }}Agent:
|
|
|
61
61
|
),
|
|
62
62
|
verbose=True,
|
|
63
63
|
allow_delegation=False,
|
|
64
|
+
llm=DEFAULT_LLM,
|
|
64
65
|
tools_subset=tools_subset
|
|
65
66
|
)
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
agentops.init(trace_name=f"Utility Agent for MCP: {self.mcp_config.name}", default_tags=[f"current time: {current_time}"])
|
|
68
69
|
def process_request(self, request: str, context: Dict[str, Any] = None) -> str:
|
|
69
70
|
"""Process a request using the MCP server capabilities."""
|
|
70
71
|
try:
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"description": "{{ agent_description }}",
|
|
4
4
|
"version": "{{ agent_version }}",
|
|
5
5
|
"agent_id": "{{ agent_id }}",
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"mcp_server": {
|
|
7
|
+
"name": "{{ mcp_server_name }}",
|
|
8
|
+
"url": "{{ mcp_server_url | replace('localhost', 'host.docker.internal') }}",
|
|
9
9
|
"description": "{{ mcp_server_description }}",
|
|
10
10
|
"server_type": "{{ mcp_server_type }}",
|
|
11
11
|
"capabilities": {{ mcp_server_capabilities | tojson }},
|