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.
Files changed (95) hide show
  1. traia_iatp/__init__.py +105 -8
  2. traia_iatp/cli/main.py +85 -1
  3. traia_iatp/client/__init__.py +28 -3
  4. traia_iatp/client/crewai_a2a_tools.py +32 -12
  5. traia_iatp/client/d402_a2a_client.py +348 -0
  6. traia_iatp/contracts/__init__.py +11 -0
  7. traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
  8. traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
  9. traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
  10. traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
  11. traia_iatp/contracts/iatp_contracts_config.py +263 -0
  12. traia_iatp/contracts/wallet_creator.py +369 -0
  13. traia_iatp/core/models.py +17 -3
  14. traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
  15. traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
  16. traia_iatp/d402/README.md +489 -0
  17. traia_iatp/d402/__init__.py +54 -0
  18. traia_iatp/d402/asgi_wrapper.py +469 -0
  19. traia_iatp/d402/chains.py +102 -0
  20. traia_iatp/d402/client.py +150 -0
  21. traia_iatp/d402/clients/__init__.py +7 -0
  22. traia_iatp/d402/clients/base.py +218 -0
  23. traia_iatp/d402/clients/httpx.py +266 -0
  24. traia_iatp/d402/common.py +114 -0
  25. traia_iatp/d402/encoding.py +28 -0
  26. traia_iatp/d402/examples/client_example.py +197 -0
  27. traia_iatp/d402/examples/server_example.py +171 -0
  28. traia_iatp/d402/facilitator.py +481 -0
  29. traia_iatp/d402/mcp_middleware.py +296 -0
  30. traia_iatp/d402/models.py +116 -0
  31. traia_iatp/d402/networks.py +98 -0
  32. traia_iatp/d402/path.py +43 -0
  33. traia_iatp/d402/payment_introspection.py +126 -0
  34. traia_iatp/d402/payment_signing.py +183 -0
  35. traia_iatp/d402/price_builder.py +164 -0
  36. traia_iatp/d402/servers/__init__.py +61 -0
  37. traia_iatp/d402/servers/base.py +139 -0
  38. traia_iatp/d402/servers/example_general_server.py +140 -0
  39. traia_iatp/d402/servers/fastapi.py +253 -0
  40. traia_iatp/d402/servers/mcp.py +304 -0
  41. traia_iatp/d402/servers/starlette.py +878 -0
  42. traia_iatp/d402/starlette_middleware.py +529 -0
  43. traia_iatp/d402/types.py +300 -0
  44. traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
  45. traia_iatp/mcp/__init__.py +3 -0
  46. traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
  47. traia_iatp/mcp/mcp_agent_template.py +78 -13
  48. traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
  49. traia_iatp/mcp/templates/README.md.j2 +104 -8
  50. traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
  51. traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
  52. traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
  53. traia_iatp/mcp/templates/env.example.j2 +60 -0
  54. traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
  55. traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
  56. traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
  57. traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
  58. traia_iatp/mcp/templates/server.py.j2 +174 -197
  59. traia_iatp/mcp/traia_mcp_adapter.py +182 -20
  60. traia_iatp/registry/__init__.py +47 -12
  61. traia_iatp/registry/atlas_search_indexes.json +108 -54
  62. traia_iatp/registry/iatp_search_api.py +169 -39
  63. traia_iatp/registry/mongodb_registry.py +241 -69
  64. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
  65. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
  66. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
  67. traia_iatp/registry/readmes/README.md +3 -3
  68. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
  69. traia_iatp/scripts/__init__.py +2 -0
  70. traia_iatp/scripts/create_wallet.py +244 -0
  71. traia_iatp/server/a2a_server.py +22 -7
  72. traia_iatp/server/iatp_server_template_generator.py +23 -0
  73. traia_iatp/server/templates/.dockerignore.j2 +48 -0
  74. traia_iatp/server/templates/Dockerfile.j2 +23 -1
  75. traia_iatp/server/templates/README.md +2 -2
  76. traia_iatp/server/templates/README.md.j2 +5 -5
  77. traia_iatp/server/templates/__main__.py.j2 +374 -66
  78. traia_iatp/server/templates/agent.py.j2 +12 -11
  79. traia_iatp/server/templates/agent_config.json.j2 +3 -3
  80. traia_iatp/server/templates/agent_executor.py.j2 +45 -27
  81. traia_iatp/server/templates/env.example.j2 +32 -4
  82. traia_iatp/server/templates/gitignore.j2 +7 -0
  83. traia_iatp/server/templates/pyproject.toml.j2 +13 -12
  84. traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
  85. traia_iatp/server/templates/server.py.j2 +197 -10
  86. traia_iatp/special_agencies/registry_search_agency.py +1 -1
  87. traia_iatp/utils/iatp_utils.py +6 -6
  88. traia_iatp-0.1.67.dist-info/METADATA +320 -0
  89. traia_iatp-0.1.67.dist-info/RECORD +117 -0
  90. traia_iatp-0.1.2.dist-info/METADATA +0 -414
  91. traia_iatp-0.1.2.dist-info/RECORD +0 -72
  92. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
  93. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
  94. {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
  95. {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
- from hypercorn.asyncio import serve
28
- from hypercorn.config import Config
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
- agent_card = AgentCard(
299
- name="{{ agent_id }}",
300
- description="{{ agent_description }}",
301
- url=f"http://0.0.0.0:{os.environ.get('PORT', 8000)}",
302
- version="{{ agent_version }}",
303
- capabilities=capabilities,
304
- skills=skills,
305
- defaultInputModes=["text", "text/plain"],
306
- defaultOutputModes=["text", "text/plain", "text/event-stream"] if supports_streaming else ["text", "text/plain"]
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 - this should add all necessary routes including JSON-RPC endpoint
326
- starlette_app = app.build()
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 main():
395
- """Main function to start the A2A server with HTTP/2 support."""
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 with HTTP/2 support")
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 Hypercorn
484
- await serve(app, config)
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(main())
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
- try:
18
- import agentops
19
- from agentops.sdk.decorators import operation
20
- AGENTOPS_AVAILABLE = True
21
- except ImportError:
22
- AGENTOPS_AVAILABLE = False
23
- agentops = None
24
- # Create a no-op decorator if AgentOps is not available
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
- @operation
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
- "mcp_server": {
7
- "name": "{{ mcp_server_name }}",
8
- "url": "{{ mcp_server_url }}",
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 }},