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
@@ -1,240 +1,217 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- {{ api_name }} MCP Server{{ auth_description }}
3
+ {{ api_name }} MCP Server - FastMCP with D402 Transport Wrapper
4
4
 
5
- A Model Context Protocol server providing {{ api_description }}
6
- using the {{ api_name }} API{{ auth_details }}.
5
+ Uses FastMCP from official MCP SDK with D402MCPTransport wrapper for HTTP 402.
6
+
7
+ Architecture:
8
+ - FastMCP for tool decorators and Context objects
9
+ - D402MCPTransport wraps the /mcp route for HTTP 402 interception
10
+ - Proper HTTP 402 status codes (not JSON-RPC wrapped)
11
+
12
+ Generated from OpenAPI: {{ docs_url }}
13
+
14
+ Environment Variables:
15
+ {% if requires_auth %}- {{ api_key_env_var }}: Server's internal API key (for paid requests)
16
+ {% endif %}- SERVER_ADDRESS: Payment address (IATP wallet contract)
17
+ - MCP_OPERATOR_PRIVATE_KEY: Operator signing key
18
+ - D402_TESTING_MODE: Skip facilitator (default: true)
7
19
  """
8
20
 
9
- import asyncio
10
- import logging
11
21
  import os
22
+ import logging
12
23
  import sys
13
24
  from typing import Dict, Any, Optional
14
25
  from datetime import datetime
15
26
 
16
- # Third-party imports
17
27
  import requests
18
- from fastmcp import FastMCP, Context
19
- from fastmcp.server.middleware import Middleware, MiddlewareContext
20
- from fastmcp.server.dependencies import get_http_request, get_context
21
- from starlette.requests import Request
22
- from starlette.responses import JSONResponse
23
28
  from retry import retry
24
- {% if sdk_package %}
25
- # {{ api_name }} SDK
26
- # TODO: Adjust the import based on the SDK documentation
27
- # Common patterns:
28
- # - from {{ sdk_module }} import Client
29
- # - from {{ sdk_module }} import {{ api_name }}Client
30
- # - import {{ sdk_module }}
31
- # Check the SDK docs for the correct import statement
32
- import {{ sdk_module }}
33
- {% endif %}
29
+ from dotenv import load_dotenv
30
+ import uvicorn
31
+
32
+ load_dotenv()
34
33
 
35
34
  # Configure logging
36
35
  logging.basicConfig(
37
- level=logging.INFO,
36
+ level=os.getenv("LOG_LEVEL", "INFO").upper(),
38
37
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
39
38
  )
40
- logger = logging.getLogger('{{ logger_name }}')
39
+ logger = logging.getLogger('{{ api_slug }}_mcp')
41
40
 
42
- # Get stage from environment (useful for different API endpoints)
43
- STAGE = os.getenv("STAGE", "MAINNET").upper()
41
+ # FastMCP from official SDK
42
+ from mcp.server.fastmcp import FastMCP, Context
43
+ from starlette.requests import Request
44
+ from starlette.responses import JSONResponse
45
+ from starlette.middleware.cors import CORSMiddleware
44
46
 
47
+ # D402 payment protocol - using Starlette middleware
48
+ from traia_iatp.d402.starlette_middleware import D402PaymentMiddleware
49
+ from traia_iatp.d402.mcp_middleware import require_payment_for_tool, get_active_api_key
50
+ from traia_iatp.d402.payment_introspection import extract_payment_configs_from_mcp
51
+ from traia_iatp.d402.types import TokenAmount, TokenAsset, EIP712Domain
52
+
53
+ # Configuration
54
+ STAGE = os.getenv("STAGE", "MAINNET").upper()
55
+ PORT = int(os.getenv("PORT", "8000"))
56
+ SERVER_ADDRESS = os.getenv("SERVER_ADDRESS")
57
+ if not SERVER_ADDRESS:
58
+ raise ValueError("SERVER_ADDRESS required for payment protocol")
45
59
 
46
60
  {% if requires_auth %}
47
- class AuthMiddleware(Middleware):
48
- """Middleware to extract and store API keys from Authorization headers."""
49
-
50
- async def on_request(self, context: MiddlewareContext, call_next):
51
- """Extract bearer token on each request and bind to context state."""
52
- try:
53
- # Access the raw HTTP request
54
- request: Request = get_http_request()
55
-
56
- # Debug: log all headers
57
- logger.info(f"AuthMiddleware: Received request with headers: {dict(request.headers)}")
58
- logger.info(f"AuthMiddleware: Method: {request.method}, URL: {request.url}")
59
-
60
- # Extract bearer token from Authorization header
61
- auth = request.headers.get("Authorization", "")
62
- token = auth[7:].strip() if auth.lower().startswith("bearer ") else None
63
-
64
- if not token:
65
- # Check X-API-KEY header as alternative
66
- token = request.headers.get("X-API-KEY", "")
67
-
68
- if token:
69
- # Store the API key in the context state
70
- # This will be accessible in tools via get_context()
71
- if hasattr(context, 'state'):
72
- context.state.api_key = token
73
- logger.info(f"API key bound to context state: {token[:10]}...")
74
- else:
75
- # Try to store it in the request state as fallback
76
- request.state.api_key = token
77
- logger.info(f"API key bound to request state: {token[:10]}...")
78
- else:
79
- logger.warning(f"No API key provided in request headers")
80
-
81
- except Exception as e:
82
- # This might happen in non-HTTP transports or if get_http_request fails
83
- logger.debug(f"Could not extract API key from request: {e}")
84
-
85
- # Proceed with the request (authentication check happens in the tools)
86
- return await call_next(context)
87
-
88
-
89
- # Initialize FastMCP server with middleware
90
- mcp = FastMCP("{{ api_name }} MCP Server", middleware=[AuthMiddleware()])
61
+ API_KEY = os.getenv("{{ api_key_env_var }}")
62
+ if not API_KEY:
63
+ logger.warning(f"⚠️ {{ api_key_env_var }} not set - payment required for all requests")
91
64
  {% else %}
92
- # Initialize FastMCP server
93
- mcp = FastMCP("{{ api_name }} MCP Server")
65
+ API_KEY = None
94
66
  {% endif %}
95
67
 
96
-
97
- # Add health check endpoint using FastMCP's custom_route
98
- @mcp.custom_route("/health", methods=["GET"])
99
- async def health_check(request: Request) -> JSONResponse:
100
- """Health check endpoint for container orchestration."""
101
- return JSONResponse(
102
- content={
103
- "status": "healthy",
104
- "service": "{{ api_slug }}-mcp-server",
105
- "timestamp": datetime.now().isoformat()
106
- }
107
- )
108
-
109
-
68
+ logger.info("="*80)
69
+ logger.info(f"{{ api_name }} MCP Server (FastMCP + D402 Wrapper)")
70
+ logger.info(f"API: {{ api_url }}")
71
+ logger.info(f"Payment: {SERVER_ADDRESS}")
110
72
  {% if requires_auth %}
111
- def get_session_api_key(context: Context) -> Optional[str]:
112
- """Get the API key for the current session."""
113
- try:
114
- # Try to get the API key from the context state
115
- # The middleware should have stored it there
116
- if hasattr(context, 'state') and hasattr(context.state, 'api_key'):
117
- return context.state.api_key
118
-
119
- # Fallback: try to get it from the current HTTP request
120
- try:
121
- request: Request = get_http_request()
122
- if hasattr(request.state, 'api_key'):
123
- return request.state.api_key
124
- except Exception:
125
- pass
126
-
127
- # If we're in a tool context, try to get the context using the dependency
128
- try:
129
- ctx = get_context()
130
- if hasattr(ctx, 'state') and hasattr(ctx.state, 'api_key'):
131
- return ctx.state.api_key
132
- except Exception:
133
- pass
134
-
135
- except Exception as e:
136
- logger.debug(f"Could not retrieve API key from context: {e}")
137
-
138
- return None
73
+ logger.info(f"API Key: {'✅' if API_KEY else '❌ Payment required'}")
139
74
  {% endif %}
75
+ logger.info("="*80)
76
+
77
+ # Create FastMCP server
78
+ mcp = FastMCP("{{ api_name }} MCP Server", host="0.0.0.0")
140
79
 
80
+ logger.info(f"✅ FastMCP server created")
141
81
 
82
+ # ============================================================================
83
+ # TOOL IMPLEMENTATIONS
84
+ # ============================================================================
85
+ # Tool implementations will be added here by endpoint_implementer_crew
86
+ # Each tool will use the @mcp.tool() and @require_payment_for_tool() decorators
142
87
  # TODO: Add your API-specific functions here
143
- # Use @retry decorator ONLY for external API calls (not internal functions):
144
- # @retry(tries=2, delay=1, backoff=2, jitter=(1, 3))
145
- # def call_{{ api_slug }}_api(query: str, api_key: str) -> Dict[str, Any]:
146
- # """Call the {{ api_name }} API with the given query."""
147
- # # You can use STAGE to determine which API endpoint to use:
148
- # # base_url = "https://api-testnet.example.com" if STAGE == "TESTNET" else "https://api.example.com"
149
- # # Implement your API logic here (HTTP requests, SDK calls, etc.)
150
- # pass
151
-
152
-
153
- @mcp.tool()
154
- async def example_tool(
155
- context: Context,
156
- query: str
157
- ) -> Dict[str, Any]:
88
+
89
+ # ============================================================================
90
+ # APPLICATION SETUP WITH STARLETTE MIDDLEWARE
91
+ # ============================================================================
92
+
93
+ def create_app_with_middleware():
158
94
  """
159
- Example tool for {{ api_name }} API.
95
+ Create Starlette app with d402 payment middleware.
160
96
 
161
- TODO: Replace this with your actual tool implementation.
162
-
163
- Args:
164
- context: MCP context (injected automatically)
165
- query: Query parameter
166
-
167
- Returns:
168
- Dictionary with results
97
+ Strategy:
98
+ 1. Get FastMCP's Starlette app via streamable_http_app()
99
+ 2. Extract payment configs from @require_payment_for_tool decorators
100
+ 3. Add Starlette middleware with extracted configs
101
+ 4. Single source of truth - no duplication!
169
102
  """
170
- {% if requires_auth %}
171
- # Check for API key
172
- api_key = get_session_api_key(context)
173
- if not api_key:
174
- return {"error": "No API key found. Please authenticate with Authorization: Bearer YOUR_API_KEY"}
175
- {% endif %}
103
+ logger.info("🔧 Creating FastMCP app with middleware...")
176
104
 
177
- # TODO: Implement your tool logic here
178
- return {
179
- "status": "success",
180
- "message": "This is a placeholder. Implement your {{ api_name }} logic here.",
181
- "query": query,
182
- "timestamp": datetime.now().isoformat()
183
- }
184
-
185
-
186
- @mcp.tool()
187
- async def get_api_info(context: Context) -> Dict[str, Any]:
188
- """
189
- Get information about the {{ api_name }} API service.
105
+ # Get FastMCP's Starlette app
106
+ app = mcp.streamable_http_app()
107
+ logger.info(f" Got FastMCP Starlette app")
190
108
 
191
- Args:
192
- context: MCP context (injected automatically)
109
+ # Extract payment configs from decorators (single source of truth!)
110
+ tool_payment_configs = extract_payment_configs_from_mcp(mcp, SERVER_ADDRESS)
111
+ logger.info(f"📊 Extracted {len(tool_payment_configs)} payment configs from @require_payment_for_tool decorators")
193
112
 
194
- Returns:
195
- Dictionary containing API information and status
196
- """
113
+ # D402 Configuration
114
+ facilitator_url = os.getenv("FACILITATOR_URL") or os.getenv("D402_FACILITATOR_URL")
115
+ operator_key = os.getenv("MCP_OPERATOR_PRIVATE_KEY")
116
+ network = os.getenv("NETWORK", "sepolia")
117
+ testing_mode = os.getenv("D402_TESTING_MODE", "false").lower() == "true"
118
+
119
+ # Log D402 configuration with prominent facilitator info
120
+ logger.info("="*60)
121
+ logger.info("D402 Payment Protocol Configuration:")
122
+ logger.info(f" Server Address: {SERVER_ADDRESS}")
123
+ logger.info(f" Network: {network}")
124
+ logger.info(f" Operator Key: {'✅ Set' if operator_key else '❌ Not set'}")
125
+ logger.info(f" Testing Mode: {'⚠️ ENABLED (bypasses facilitator)' if testing_mode else '✅ DISABLED (uses facilitator)'}")
126
+ logger.info("="*60)
127
+
128
+ if not facilitator_url and not testing_mode:
129
+ logger.error("❌ FACILITATOR_URL required when testing_mode is disabled!")
130
+ raise ValueError("Set FACILITATOR_URL or enable D402_TESTING_MODE=true")
131
+
132
+ if facilitator_url:
133
+ logger.info(f"🌐 FACILITATOR: {facilitator_url}")
134
+ if "localhost" in facilitator_url or "127.0.0.1" in facilitator_url or "host.docker.internal" in facilitator_url:
135
+ logger.info(f" 📍 Using LOCAL facilitator for development")
136
+ else:
137
+ logger.info(f" 🌍 Using REMOTE facilitator for production")
138
+ else:
139
+ logger.warning("⚠️ D402 Testing Mode - Facilitator bypassed")
140
+ logger.info("="*60)
141
+
142
+ # Add CORS middleware first (processes before other middleware)
143
+ app.add_middleware(
144
+ CORSMiddleware,
145
+ allow_origins=["*"], # Allow all origins
146
+ allow_credentials=True,
147
+ allow_methods=["*"], # Allow all methods
148
+ allow_headers=["*"], # Allow all headers
149
+ expose_headers=["mcp-session-id"], # Expose custom headers to browser
150
+ )
151
+ logger.info("✅ Added CORS middleware (allow all origins, expose mcp-session-id)")
152
+
153
+ # Add D402 payment middleware with extracted configs
154
+ app.add_middleware(
155
+ D402PaymentMiddleware,
156
+ tool_payment_configs=tool_payment_configs,
157
+ server_address=SERVER_ADDRESS,
158
+ {% if requires_auth %}
159
+ requires_auth=True, # Extracts API keys + checks payment
160
+ internal_api_key=API_KEY, # Server's internal key (for Mode 2: paid access)
161
+ {% else %}
162
+ requires_auth=False, # Only checks payment
163
+ {% endif %}
164
+ testing_mode=testing_mode,
165
+ facilitator_url=facilitator_url,
166
+ facilitator_api_key=os.getenv("D402_FACILITATOR_API_KEY"),
167
+ server_name="{{ api_slug }}-mcp-server" # MCP server ID for tracking
168
+ )
169
+ logger.info("✅ Added D402PaymentMiddleware")
197
170
  {% if requires_auth %}
198
- # Check authentication status
199
- api_key = get_session_api_key(context)
200
- auth_status = "authenticated" if api_key else "not authenticated"
171
+ logger.info(" - Auth extraction: Enabled")
172
+ logger.info(" - Dual mode: API key OR payment")
173
+ {% else %}
174
+ logger.info(" - Payment-only mode")
201
175
  {% endif %}
202
176
 
203
- return {
204
- "status": "ready",
205
- {% if requires_auth %}"auth_status": auth_status,{% endif %}
206
- "api_name": "{{ api_name }}",
207
- "api_url": "{{ api_url }}",
208
- "documentation": "{{ docs_url }}",
209
- "description": "{{ api_description|capitalize }}",
210
- {% if requires_auth %}"authentication": "Bearer token required in Authorization header"{% else %}"authentication": "No authentication required"{% endif %}
211
- }
212
-
213
-
214
- def run_server():
215
- """Entry point for the executable script"""
216
- logger.info("Starting {{ api_name }} MCP server{{ auth_description }}...")
177
+ # Add health check endpoint (bypasses middleware)
178
+ @app.route("/health", methods=["GET"])
179
+ async def health_check(request: Request) -> JSONResponse:
180
+ """Health check endpoint for container orchestration."""
181
+ return JSONResponse(
182
+ content={
183
+ "status": "healthy",
184
+ "service": "{{ api_slug }}-mcp-server",
185
+ "timestamp": datetime.now().isoformat()
186
+ }
187
+ )
188
+ logger.info("✅ Added /health endpoint")
189
+
190
+ return app
191
+
192
+ if __name__ == "__main__":
193
+ logger.info("="*80)
194
+ logger.info(f"Starting {{ api_name }} MCP Server")
195
+ logger.info("="*80)
196
+ logger.info("Architecture:")
197
+ logger.info(" 1. D402PaymentMiddleware intercepts requests")
217
198
  {% if requires_auth %}
218
- logger.info("Authentication: Clients must provide Authorization: Bearer YOUR_API_KEY")
199
+ logger.info(" - Extracts API keys from Authorization header")
200
+ logger.info(" - Checks payment → HTTP 402 if no API key AND no payment")
201
+ {% else %}
202
+ logger.info(" - Checks payment → HTTP 402 if missing")
219
203
  {% endif %}
204
+ logger.info(" 2. FastMCP processes valid requests with tool decorators")
205
+ logger.info("="*80)
220
206
 
221
- # Get configuration from environment
222
- port = int(os.getenv("PORT", "8000"))
223
- log_level = os.getenv("LOG_LEVEL", "INFO")
224
- logger.info(f"Server will listen on port {port}")
207
+ # Create app with middleware
208
+ app = create_app_with_middleware()
225
209
 
226
- try:
227
- mcp.run(
228
- transport="streamable-http",
229
- port=port,
230
- host="0.0.0.0",
231
- path="/mcp",
232
- log_level=log_level.lower()
233
- )
234
- except Exception as e:
235
- logger.error(f"Error starting MCP server: {e}")
236
- raise
237
-
210
+ # Run with uvicorn
211
+ uvicorn.run(
212
+ app,
213
+ host="0.0.0.0",
214
+ port=PORT,
215
+ log_level=os.getenv("LOG_LEVEL", "info").lower()
216
+ )
238
217
 
239
- if __name__ == "__main__":
240
- run_server()