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
|
@@ -1,240 +1,217 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
{{ api_name }} MCP Server
|
|
3
|
+
{{ api_name }} MCP Server - FastMCP with D402 Transport Wrapper
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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=
|
|
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('{{
|
|
39
|
+
logger = logging.getLogger('{{ api_slug }}_mcp')
|
|
41
40
|
|
|
42
|
-
#
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
93
|
-
mcp = FastMCP("{{ api_name }} MCP Server")
|
|
65
|
+
API_KEY = None
|
|
94
66
|
{% endif %}
|
|
95
67
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
95
|
+
Create Starlette app with d402 payment middleware.
|
|
160
96
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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("
|
|
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
|
-
#
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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()
|