traia-iatp 0.1.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of traia-iatp might be problematic. Click here for more details.
- traia_iatp/README.md +368 -0
- traia_iatp/__init__.py +54 -0
- traia_iatp/cli/__init__.py +5 -0
- traia_iatp/cli/main.py +483 -0
- traia_iatp/client/__init__.py +10 -0
- traia_iatp/client/a2a_client.py +274 -0
- traia_iatp/client/crewai_a2a_tools.py +335 -0
- traia_iatp/client/d402_a2a_client.py +293 -0
- traia_iatp/client/grpc_a2a_tools.py +349 -0
- traia_iatp/client/root_path_a2a_client.py +1 -0
- traia_iatp/contracts/__init__.py +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +255 -0
- traia_iatp/core/__init__.py +43 -0
- traia_iatp/core/models.py +172 -0
- traia_iatp/d402/__init__.py +55 -0
- traia_iatp/d402/chains.py +102 -0
- traia_iatp/d402/client.py +150 -0
- traia_iatp/d402/clients/__init__.py +7 -0
- traia_iatp/d402/clients/base.py +218 -0
- traia_iatp/d402/clients/httpx.py +219 -0
- traia_iatp/d402/common.py +114 -0
- traia_iatp/d402/encoding.py +28 -0
- traia_iatp/d402/examples/client_example.py +197 -0
- traia_iatp/d402/examples/server_example.py +171 -0
- traia_iatp/d402/facilitator.py +453 -0
- traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
- traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
- traia_iatp/d402/fastmcp_middleware.py +147 -0
- traia_iatp/d402/mcp_middleware.py +434 -0
- traia_iatp/d402/middleware.py +193 -0
- traia_iatp/d402/models.py +116 -0
- traia_iatp/d402/networks.py +98 -0
- traia_iatp/d402/path.py +43 -0
- traia_iatp/d402/payment_introspection.py +104 -0
- traia_iatp/d402/payment_signing.py +178 -0
- traia_iatp/d402/paywall.py +119 -0
- traia_iatp/d402/starlette_middleware.py +326 -0
- traia_iatp/d402/template.py +1 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/__init__.py +18 -0
- traia_iatp/mcp/client.py +201 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
- traia_iatp/mcp/mcp_agent_template.py +481 -0
- traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
- traia_iatp/mcp/templates/README.md.j2 +310 -0
- traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
- traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
- traia_iatp/mcp/templates/dockerignore.j2 +47 -0
- traia_iatp/mcp/templates/env.example.j2 +57 -0
- traia_iatp/mcp/templates/gitignore.j2 +77 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
- traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
- traia_iatp/mcp/templates/server.py.j2 +175 -0
- traia_iatp/mcp/traia_mcp_adapter.py +543 -0
- traia_iatp/preview_diagrams.html +181 -0
- traia_iatp/registry/__init__.py +26 -0
- traia_iatp/registry/atlas_search_indexes.json +280 -0
- traia_iatp/registry/embeddings.py +298 -0
- traia_iatp/registry/iatp_search_api.py +846 -0
- traia_iatp/registry/mongodb_registry.py +771 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
- traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
- traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
- traia_iatp/registry/readmes/README.md +251 -0
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/__init__.py +15 -0
- traia_iatp/server/a2a_server.py +219 -0
- traia_iatp/server/example_template_usage.py +72 -0
- traia_iatp/server/iatp_server_agent_generator.py +237 -0
- traia_iatp/server/iatp_server_template_generator.py +235 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +49 -0
- traia_iatp/server/templates/README.md +137 -0
- traia_iatp/server/templates/README.md.j2 +425 -0
- traia_iatp/server/templates/__init__.py +1 -0
- traia_iatp/server/templates/__main__.py.j2 +565 -0
- traia_iatp/server/templates/agent.py.j2 +94 -0
- traia_iatp/server/templates/agent_config.json.j2 +22 -0
- traia_iatp/server/templates/agent_executor.py.j2 +279 -0
- traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
- traia_iatp/server/templates/env.example.j2 +84 -0
- traia_iatp/server/templates/gitignore.j2 +78 -0
- traia_iatp/server/templates/grpc_server.py.j2 +218 -0
- traia_iatp/server/templates/pyproject.toml.j2 +78 -0
- traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
- traia_iatp/server/templates/server.py.j2 +243 -0
- traia_iatp/special_agencies/__init__.py +4 -0
- traia_iatp/special_agencies/registry_search_agency.py +392 -0
- traia_iatp/utils/__init__.py +10 -0
- traia_iatp/utils/docker_utils.py +251 -0
- traia_iatp/utils/general.py +64 -0
- traia_iatp/utils/iatp_utils.py +126 -0
- traia_iatp-0.1.29.dist-info/METADATA +423 -0
- traia_iatp-0.1.29.dist-info/RECORD +107 -0
- traia_iatp-0.1.29.dist-info/WHEEL +5 -0
- traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
- traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
- traia_iatp-0.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastMCP-specific middleware for d402 payment protocol.
|
|
3
|
+
|
|
4
|
+
This module contains middleware classes for standalone FastMCP package.
|
|
5
|
+
These are NOT used with the official MCP SDK - see starlette_middleware.py instead.
|
|
6
|
+
|
|
7
|
+
DEPRECATED: Use starlette_middleware.py for official MCP SDK.
|
|
8
|
+
This file is kept for backwards compatibility with standalone fastmcp package.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
from starlette.responses import JSONResponse
|
|
17
|
+
|
|
18
|
+
from .types import PaymentPayload
|
|
19
|
+
from .encoding import safe_base64_decode
|
|
20
|
+
from .facilitator import IATPSettlementFacilitator
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Import FastMCP middleware classes (only available with standalone fastmcp)
|
|
25
|
+
try:
|
|
26
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
27
|
+
FASTMCP_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
# FastMCP not available - this module won't work
|
|
30
|
+
logger.warning("Standalone fastmcp not available - fastmcp_middleware.py will not work")
|
|
31
|
+
logger.warning("Use starlette_middleware.py for official MCP SDK instead")
|
|
32
|
+
Middleware = None
|
|
33
|
+
MiddlewareContext = None
|
|
34
|
+
FASTMCP_AVAILABLE = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class D402PaymentRequiredException(Exception):
|
|
38
|
+
"""Exception raised when payment is required (HTTP 402)."""
|
|
39
|
+
def __init__(self, payment_response: dict):
|
|
40
|
+
self.payment_response = payment_response
|
|
41
|
+
super().__init__("Payment required")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if FASTMCP_AVAILABLE and Middleware is not None:
|
|
45
|
+
class D402MCPMiddleware(Middleware):
|
|
46
|
+
"""
|
|
47
|
+
HTTP 402 Payment Required middleware for standalone FastMCP servers.
|
|
48
|
+
|
|
49
|
+
DEPRECATED: Use D402PaymentMiddleware from starlette_middleware.py for official MCP SDK.
|
|
50
|
+
|
|
51
|
+
This is kept for backwards compatibility with standalone fastmcp package.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
server_address: str,
|
|
57
|
+
facilitator_url: Optional[str] = None,
|
|
58
|
+
facilitator_api_key: Optional[str] = None,
|
|
59
|
+
verify_signatures: bool = True,
|
|
60
|
+
testing_mode: bool = False,
|
|
61
|
+
requires_auth: bool = False,
|
|
62
|
+
internal_api_key_env_var: Optional[str] = None
|
|
63
|
+
):
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
self.server_address = server_address
|
|
67
|
+
self.facilitator_url = facilitator_url or os.getenv("D402_FACILITATOR_URL", "https://facilitator.d402.net")
|
|
68
|
+
self.facilitator_api_key = facilitator_api_key or os.getenv("D402_FACILITATOR_API_KEY")
|
|
69
|
+
self.verify_signatures = verify_signatures
|
|
70
|
+
self.testing_mode = testing_mode or os.getenv("D402_TESTING_MODE", "false").lower() == "true"
|
|
71
|
+
self.requires_auth = requires_auth
|
|
72
|
+
self.internal_api_key_env_var = internal_api_key_env_var
|
|
73
|
+
|
|
74
|
+
# Initialize facilitator
|
|
75
|
+
self.facilitator = None
|
|
76
|
+
if not self.testing_mode:
|
|
77
|
+
try:
|
|
78
|
+
operator_private_key = os.getenv("MCP_OPERATOR_PRIVATE_KEY") or os.getenv("OPERATOR_PRIVATE_KEY")
|
|
79
|
+
self.facilitator = IATPSettlementFacilitator(
|
|
80
|
+
relayer_url=self.facilitator_url,
|
|
81
|
+
relayer_api_key=self.facilitator_api_key,
|
|
82
|
+
provider_operator_key=operator_private_key,
|
|
83
|
+
facilitator_url=self.facilitator_url,
|
|
84
|
+
facilitator_api_key=self.facilitator_api_key
|
|
85
|
+
)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning(f"Could not initialize facilitator: {e}")
|
|
88
|
+
self.testing_mode = True
|
|
89
|
+
|
|
90
|
+
logger.info(f"D402 MCP Middleware initialized (FastMCP - deprecated)")
|
|
91
|
+
|
|
92
|
+
async def on_request(self, context: MiddlewareContext, call_next):
|
|
93
|
+
"""FastMCP middleware on_request handler."""
|
|
94
|
+
# Full implementation would go here
|
|
95
|
+
# For now, just pass through
|
|
96
|
+
logger.warning("D402MCPMiddleware for standalone fastmcp is deprecated")
|
|
97
|
+
logger.warning("Use D402PaymentMiddleware from starlette_middleware.py instead")
|
|
98
|
+
return await call_next(context)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def create_d402_mcp_middleware(
|
|
102
|
+
server_address: Optional[str] = None,
|
|
103
|
+
facilitator_url: Optional[str] = None,
|
|
104
|
+
facilitator_api_key: Optional[str] = None,
|
|
105
|
+
verify_signatures: bool = True,
|
|
106
|
+
testing_mode: bool = False,
|
|
107
|
+
requires_auth: bool = False,
|
|
108
|
+
internal_api_key_env_var: Optional[str] = None
|
|
109
|
+
) -> D402MCPMiddleware:
|
|
110
|
+
"""
|
|
111
|
+
Create d402 middleware for standalone FastMCP.
|
|
112
|
+
|
|
113
|
+
DEPRECATED: Use D402PaymentMiddleware from starlette_middleware.py for official MCP SDK.
|
|
114
|
+
"""
|
|
115
|
+
server_address = (
|
|
116
|
+
server_address or
|
|
117
|
+
os.getenv("SERVER_ADDRESS") or
|
|
118
|
+
os.getenv("PAYMENT_ADDRESS") or
|
|
119
|
+
os.getenv("EVM_ADDRESS")
|
|
120
|
+
)
|
|
121
|
+
if not server_address:
|
|
122
|
+
raise ValueError("server_address is required")
|
|
123
|
+
|
|
124
|
+
return D402MCPMiddleware(
|
|
125
|
+
server_address=server_address,
|
|
126
|
+
facilitator_url=facilitator_url,
|
|
127
|
+
facilitator_api_key=facilitator_api_key,
|
|
128
|
+
verify_signatures=verify_signatures,
|
|
129
|
+
testing_mode=testing_mode,
|
|
130
|
+
requires_auth=requires_auth,
|
|
131
|
+
internal_api_key_env_var=internal_api_key_env_var
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
# FastMCP not available - create stub
|
|
136
|
+
class D402MCPMiddleware:
|
|
137
|
+
"""Stub class when FastMCP not available."""
|
|
138
|
+
def __init__(self, *args, **kwargs):
|
|
139
|
+
raise ImportError("Standalone fastmcp not available. Use starlette_middleware.py for official MCP SDK.")
|
|
140
|
+
|
|
141
|
+
def create_d402_mcp_middleware(*args, **kwargs):
|
|
142
|
+
"""Stub function when FastMCP not available."""
|
|
143
|
+
raise ImportError("Standalone fastmcp not available. Use starlette_middleware.py for official MCP SDK.")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = ["D402MCPMiddleware", "D402PaymentRequiredException", "create_d402_mcp_middleware"]
|
|
147
|
+
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""D402 payment helpers for MCP servers with official SDK.
|
|
2
|
+
|
|
3
|
+
This module provides decorators and helper functions for d402 payment protocol.
|
|
4
|
+
Works with official MCP SDK (mcp.server.fastmcp).
|
|
5
|
+
|
|
6
|
+
FastMCP Middleware classes are in fastmcp_middleware.py (deprecated).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional, Dict, Any, Callable
|
|
12
|
+
from functools import wraps
|
|
13
|
+
|
|
14
|
+
# Import Context from official SDK
|
|
15
|
+
from mcp.server.fastmcp import Context
|
|
16
|
+
|
|
17
|
+
from starlette.requests import Request
|
|
18
|
+
from starlette.responses import JSONResponse
|
|
19
|
+
from web3 import Web3
|
|
20
|
+
|
|
21
|
+
from .types import PaymentPayload, Price, TokenAmount, TokenAsset, EIP712Domain, PaymentRequirements, d402PaymentRequiredResponse
|
|
22
|
+
from .encoding import safe_base64_decode
|
|
23
|
+
from .common import process_price_to_atomic_amount, d402_VERSION
|
|
24
|
+
from .facilitator import IATPSettlementFacilitator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class D402PaymentRequiredException(Exception):
|
|
28
|
+
"""Exception raised when payment is required (HTTP 402)."""
|
|
29
|
+
def __init__(self, payment_response: Dict[str, Any]):
|
|
30
|
+
self.payment_response = payment_response
|
|
31
|
+
super().__init__("Payment required")
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EndpointPaymentInfo:
|
|
37
|
+
"""Payment information for a specific endpoint."""
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
settlement_token_address: str,
|
|
41
|
+
settlement_token_network: str,
|
|
42
|
+
payment_price_float: float,
|
|
43
|
+
payment_price_wei: str,
|
|
44
|
+
server_address: str
|
|
45
|
+
):
|
|
46
|
+
self.settlement_token_address = settlement_token_address
|
|
47
|
+
self.settlement_token_network = settlement_token_network
|
|
48
|
+
self.payment_price_float = payment_price_float
|
|
49
|
+
self.payment_price_wei = payment_price_wei
|
|
50
|
+
self.server_address = server_address
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_active_api_key(context: Any) -> Optional[str]:
|
|
54
|
+
"""
|
|
55
|
+
Get the API key to use for calling external APIs.
|
|
56
|
+
|
|
57
|
+
Returns api_key_to_use which was set by:
|
|
58
|
+
1. D402PaymentMiddleware (in request.state)
|
|
59
|
+
2. @require_payment_for_tool decorator (copied to context.state)
|
|
60
|
+
|
|
61
|
+
Priority:
|
|
62
|
+
1. context.state.api_key_to_use (set by decorator)
|
|
63
|
+
2. request.state.api_key_to_use (set by middleware)
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
context: MCP context object
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
API key string (client's OR server's) if authorized, None otherwise
|
|
70
|
+
|
|
71
|
+
Usage in tools:
|
|
72
|
+
api_key = get_active_api_key(context)
|
|
73
|
+
if api_key:
|
|
74
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
# Check request.state (set by middleware)
|
|
78
|
+
# Context is a Pydantic model - we can't set arbitrary fields on it
|
|
79
|
+
# So we read directly from request.state where middleware stored it
|
|
80
|
+
logger.debug(f"get_active_api_key: Checking context type={type(context).__name__}")
|
|
81
|
+
logger.debug(f" has request_context: {hasattr(context, 'request_context')}")
|
|
82
|
+
|
|
83
|
+
if hasattr(context, 'request_context') and context.request_context:
|
|
84
|
+
logger.debug(f" request_context exists")
|
|
85
|
+
if hasattr(context.request_context, 'request') and context.request_context.request:
|
|
86
|
+
request = context.request_context.request
|
|
87
|
+
logger.debug(f" request exists, has state: {hasattr(request, 'state')}")
|
|
88
|
+
if hasattr(request, 'state'):
|
|
89
|
+
api_key = getattr(request.state, 'api_key_to_use', None)
|
|
90
|
+
logger.debug(f" api_key_to_use: {api_key[:10] if api_key else None}")
|
|
91
|
+
if api_key:
|
|
92
|
+
return api_key
|
|
93
|
+
|
|
94
|
+
logger.warning(f"get_active_api_key: Could not find api_key_to_use in request.state")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"get_active_api_key error: {e}")
|
|
98
|
+
import traceback
|
|
99
|
+
logger.error(traceback.format_exc())
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def settle_payment(
|
|
105
|
+
context: Any,
|
|
106
|
+
endpoint_info: EndpointPaymentInfo,
|
|
107
|
+
output_data: Any,
|
|
108
|
+
middleware: Optional[Any] = None # Middleware instance (Starlette or FastMCP)
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Settle a payment after successful API call with output hash attestation.
|
|
112
|
+
|
|
113
|
+
Complete 402 settlement flow:
|
|
114
|
+
1. Hash the output data (result returned to client)
|
|
115
|
+
2. Provider signs over output_hash + consumer_request
|
|
116
|
+
3. Submit to facilitator with proof of service completion
|
|
117
|
+
4. Facilitator submits to IATP Settlement Layer on-chain
|
|
118
|
+
|
|
119
|
+
This should be called AFTER the tool successfully processes the request
|
|
120
|
+
to submit the payment settlement to the facilitator/blockchain.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
context: MCP context (contains payment_payload)
|
|
124
|
+
endpoint_info: Endpoint payment requirements
|
|
125
|
+
output_data: The actual output/result being returned to client (will be hashed)
|
|
126
|
+
middleware: Optional D402MCPMiddleware instance (for facilitator access)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
bool: True if settlement submitted successfully
|
|
130
|
+
|
|
131
|
+
Usage in tools (for production settlement):
|
|
132
|
+
# Execute API call
|
|
133
|
+
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
134
|
+
result = response.json()
|
|
135
|
+
|
|
136
|
+
# Settle payment with output hash
|
|
137
|
+
if context.state.payment_payload:
|
|
138
|
+
await settle_payment(context, endpoint_payment, output_data=result, middleware=...)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
payment_payload = getattr(context.state, 'payment_payload', None) if hasattr(context, 'state') else None
|
|
144
|
+
if not payment_payload:
|
|
145
|
+
logger.debug("No payment to settle (authenticated mode)")
|
|
146
|
+
return True # Not an error - client used their own API key
|
|
147
|
+
|
|
148
|
+
# Skip settlement in testing mode
|
|
149
|
+
if middleware and middleware.testing_mode:
|
|
150
|
+
logger.info("⚠️ Testing mode: Skipping payment settlement")
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
# Step 1: Hash the output data (proof of service completion)
|
|
154
|
+
import json
|
|
155
|
+
from web3 import Web3
|
|
156
|
+
|
|
157
|
+
logger.info("🔐 Starting payment settlement process...")
|
|
158
|
+
|
|
159
|
+
# Serialize output to JSON and hash it
|
|
160
|
+
output_json = json.dumps(output_data, sort_keys=True, separators=(',', ':'))
|
|
161
|
+
output_hash = Web3.keccak(text=output_json).hex()
|
|
162
|
+
logger.info(f"📊 Output data serialized: {len(output_json)} bytes")
|
|
163
|
+
logger.info(f"🔑 Output hash calculated: {output_hash}")
|
|
164
|
+
logger.info(f" First 1000 chars of output: {output_json[:1000]}")
|
|
165
|
+
|
|
166
|
+
# Step 2: Get payment_uuid from context (from facilitator verify response)
|
|
167
|
+
# The payment_uuid is the primary payment identifier from the facilitator
|
|
168
|
+
# It was set in verify_endpoint_payment() after facilitator.verify() returned it
|
|
169
|
+
payment_uuid = None
|
|
170
|
+
if hasattr(context, 'state') and hasattr(context.state, 'payment_uuid'):
|
|
171
|
+
payment_uuid = context.state.payment_uuid
|
|
172
|
+
|
|
173
|
+
if not payment_uuid:
|
|
174
|
+
logger.warning("No payment_uuid found in context - payment may not have been verified via facilitator")
|
|
175
|
+
|
|
176
|
+
# Step 3: Get facilitator fee from context (set by verify response)
|
|
177
|
+
facilitator_fee_percent = 250 # Default
|
|
178
|
+
if hasattr(context, 'state') and hasattr(context.state, 'facilitator_fee_percent'):
|
|
179
|
+
facilitator_fee_percent = context.state.facilitator_fee_percent
|
|
180
|
+
|
|
181
|
+
# Step 4: Create PaymentRequirements for this endpoint
|
|
182
|
+
# Include output_hash, payment_uuid, and facilitatorFeePercent in extra data
|
|
183
|
+
extra_data = {
|
|
184
|
+
"output_hash": output_hash,
|
|
185
|
+
"facilitator_fee_percent": facilitator_fee_percent
|
|
186
|
+
}
|
|
187
|
+
if payment_uuid:
|
|
188
|
+
extra_data["payment_uuid"] = payment_uuid
|
|
189
|
+
|
|
190
|
+
payment_requirements = PaymentRequirements(
|
|
191
|
+
scheme="exact",
|
|
192
|
+
network=endpoint_info.settlement_token_network,
|
|
193
|
+
pay_to=endpoint_info.server_address,
|
|
194
|
+
max_amount_required=endpoint_info.payment_price_wei,
|
|
195
|
+
max_timeout_seconds=300,
|
|
196
|
+
description=f"Service completed - output_hash: {output_hash}",
|
|
197
|
+
resource="",
|
|
198
|
+
mime_type="application/json",
|
|
199
|
+
asset=endpoint_info.settlement_token_address,
|
|
200
|
+
extra=extra_data # Include output hash, payment_uuid, and facilitatorFeePercent
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Step 4: Settle via facilitator
|
|
204
|
+
# The facilitator will:
|
|
205
|
+
# - Create provider attestation signing over the consumer's request + output_hash
|
|
206
|
+
# - Submit to relayer with proof of service completion
|
|
207
|
+
# - Relayer submits to IATPSettlementLayer on-chain
|
|
208
|
+
if middleware and middleware.facilitator:
|
|
209
|
+
try:
|
|
210
|
+
logger.info(f"📤 Submitting settlement to facilitator...")
|
|
211
|
+
logger.info(f" Payment UUID: {payment_uuid if payment_uuid else 'N/A'}")
|
|
212
|
+
logger.info(f" Output hash: {output_hash}")
|
|
213
|
+
logger.info(f" Amount: {endpoint_info.payment_price_wei} wei")
|
|
214
|
+
|
|
215
|
+
settle_result = await middleware.facilitator.settle(payment_payload, payment_requirements)
|
|
216
|
+
if settle_result.success:
|
|
217
|
+
logger.info(f"✅ Payment SETTLEMENT COMPLETE:")
|
|
218
|
+
logger.info(f" Transaction: {settle_result.transaction}")
|
|
219
|
+
logger.info(f" Network: {settle_result.network}")
|
|
220
|
+
logger.info(f" Payer: {settle_result.payer}")
|
|
221
|
+
logger.info(f" Output Hash: {output_hash}")
|
|
222
|
+
logger.info(f" Status: SETTLED ON-CHAIN")
|
|
223
|
+
return True
|
|
224
|
+
else:
|
|
225
|
+
logger.error(f"❌ Payment settlement FAILED: {settle_result.error_reason}")
|
|
226
|
+
# TODO: Queue for retry
|
|
227
|
+
return False
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Error settling payment via facilitator: {e}")
|
|
230
|
+
# Don't fail the request if settlement fails
|
|
231
|
+
# Settlement can be retried later
|
|
232
|
+
logger.warning("Settlement failed but request completed - will retry later")
|
|
233
|
+
# TODO: Queue settlement for retry
|
|
234
|
+
return False
|
|
235
|
+
else:
|
|
236
|
+
logger.warning("No facilitator available for settlement")
|
|
237
|
+
# TODO: Queue settlement for later retry
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
import traceback
|
|
242
|
+
logger.error(f"Error in settle_payment: {e}")
|
|
243
|
+
logger.error(f"Traceback:\n{traceback.format_exc()}")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def require_payment_for_tool(
|
|
248
|
+
price: Price,
|
|
249
|
+
description: str = ""
|
|
250
|
+
):
|
|
251
|
+
"""
|
|
252
|
+
Decorator for MCP tools that require payment (like FastAPI's require_payment).
|
|
253
|
+
|
|
254
|
+
Matches the pattern from IATPx402/e2e/servers/fastapi/main.py where payment
|
|
255
|
+
is configured OUTSIDE the endpoint function.
|
|
256
|
+
|
|
257
|
+
Global config (from middleware):
|
|
258
|
+
- server_address: Where payments are sent (configured at middleware init)
|
|
259
|
+
- requires_auth: Whether API needs auth (configured at middleware init)
|
|
260
|
+
- internal_api_key_env_var: Which env var has API key (configured at middleware init)
|
|
261
|
+
|
|
262
|
+
Per-endpoint config (decorator params):
|
|
263
|
+
- price: Payment price (TokenAmount with network info, or "$0.001" string) - per-endpoint
|
|
264
|
+
- description: Description of what is being purchased - per-endpoint
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
price: Payment price (TokenAmount with asset.network, or "$0.001" string) - per-endpoint
|
|
268
|
+
When TokenAmount is used, settlement_token_address and settlement_token_network
|
|
269
|
+
are extracted from price.asset.address and price.asset.network
|
|
270
|
+
description: Description of what is being purchased - per-endpoint
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Decorator function
|
|
274
|
+
|
|
275
|
+
Usage - API requires auth (dual-mode):
|
|
276
|
+
@mcp.tool()
|
|
277
|
+
@require_payment_for_tool(
|
|
278
|
+
price=TokenAmount(
|
|
279
|
+
amount="1000",
|
|
280
|
+
asset=TokenAsset(
|
|
281
|
+
address="0xUSDC...",
|
|
282
|
+
decimals=6,
|
|
283
|
+
network="base-sepolia",
|
|
284
|
+
eip712=EIP712Domain(name="USD Coin", version="2")
|
|
285
|
+
)
|
|
286
|
+
),
|
|
287
|
+
description="Get data"
|
|
288
|
+
)
|
|
289
|
+
async def get_data(context: Context) -> Dict[str, Any]:
|
|
290
|
+
api_key = context.state.api_key_to_use # Client's OR server's API key
|
|
291
|
+
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
|
|
292
|
+
return response.json()
|
|
293
|
+
|
|
294
|
+
Usage - API doesn't require auth (payment-only):
|
|
295
|
+
@mcp.tool()
|
|
296
|
+
@require_payment_for_tool(
|
|
297
|
+
price=TokenAmount(
|
|
298
|
+
amount="1000",
|
|
299
|
+
asset=TokenAsset(
|
|
300
|
+
address="0xUSDC...",
|
|
301
|
+
decimals=6,
|
|
302
|
+
network="base-sepolia",
|
|
303
|
+
eip712=EIP712Domain(name="USD Coin", version="2")
|
|
304
|
+
)
|
|
305
|
+
),
|
|
306
|
+
description="Get public data"
|
|
307
|
+
)
|
|
308
|
+
async def get_public_data(context: Context) -> Dict[str, Any]:
|
|
309
|
+
# No API key needed - just call the API
|
|
310
|
+
response = requests.get(url)
|
|
311
|
+
return response.json()
|
|
312
|
+
"""
|
|
313
|
+
def decorator(func: Callable):
|
|
314
|
+
@wraps(func)
|
|
315
|
+
async def wrapper(context: Context, *args, **kwargs):
|
|
316
|
+
"""
|
|
317
|
+
Decorator for official MCP SDK with Starlette middleware.
|
|
318
|
+
|
|
319
|
+
1. Copies api_key_to_use from request.state to context.state (for tool access)
|
|
320
|
+
2. Calls tool function
|
|
321
|
+
3. Schedules async settlement if client paid
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# Get request from context for settlement
|
|
325
|
+
request = None
|
|
326
|
+
if hasattr(context, 'request_context') and context.request_context:
|
|
327
|
+
if hasattr(context.request_context, 'request'):
|
|
328
|
+
request = context.request_context.request
|
|
329
|
+
|
|
330
|
+
# NOTE: Don't try to set context.state - Context is immutable Pydantic model
|
|
331
|
+
# Tools should use get_active_api_key(context) which reads from request.state
|
|
332
|
+
|
|
333
|
+
# Call the actual tool function
|
|
334
|
+
logger.info("=" * 80)
|
|
335
|
+
logger.info(f"TOOL CALL STARTED: {func.__name__}")
|
|
336
|
+
logger.info("=" * 80)
|
|
337
|
+
logger.info(f"Arguments: {kwargs}")
|
|
338
|
+
#==============================================================
|
|
339
|
+
result = await func(context, *args, **kwargs)
|
|
340
|
+
#==============================================================
|
|
341
|
+
logger.info(f"✅ Tool execution complete: {func.__name__}")
|
|
342
|
+
|
|
343
|
+
# Log tool result for debugging
|
|
344
|
+
logger.info("-" * 80)
|
|
345
|
+
logger.info(f"📤 TOOL RESULT:")
|
|
346
|
+
result_str = str(result)
|
|
347
|
+
if len(result_str) > 1000:
|
|
348
|
+
logger.info(f"{result_str[:1000]}...")
|
|
349
|
+
logger.info(f" (Result truncated - {len(result_str)} total chars)")
|
|
350
|
+
else:
|
|
351
|
+
logger.info(result_str)
|
|
352
|
+
logger.info("=" * 80)
|
|
353
|
+
|
|
354
|
+
# Auto-settle payment ASYNCHRONOUSLY if client paid
|
|
355
|
+
if request and hasattr(request, 'state'):
|
|
356
|
+
payment_validated = getattr(request.state, 'payment_validated', False)
|
|
357
|
+
payment_uuid = getattr(request.state, 'payment_uuid', None)
|
|
358
|
+
|
|
359
|
+
if payment_validated:
|
|
360
|
+
# Client paid - schedule settlement (fire-and-forget)
|
|
361
|
+
logger.info(f"💰 Payment detected for {func.__name__} - scheduling settlement (async)...")
|
|
362
|
+
|
|
363
|
+
# Get payment and endpoint info for settlement
|
|
364
|
+
if isinstance(price, TokenAmount):
|
|
365
|
+
payment_price_wei = price.amount
|
|
366
|
+
payment_price_float = float(price.amount) / (10 ** price.asset.decimals)
|
|
367
|
+
settlement_token_address = price.asset.address
|
|
368
|
+
settlement_token_network = price.asset.network
|
|
369
|
+
|
|
370
|
+
# Get server address from env (set in template)
|
|
371
|
+
server_address = os.getenv("SERVER_ADDRESS")
|
|
372
|
+
|
|
373
|
+
if server_address:
|
|
374
|
+
endpoint_info = EndpointPaymentInfo(
|
|
375
|
+
settlement_token_address=settlement_token_address,
|
|
376
|
+
settlement_token_network=settlement_token_network,
|
|
377
|
+
payment_price_float=payment_price_float,
|
|
378
|
+
payment_price_wei=payment_price_wei,
|
|
379
|
+
server_address=server_address
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Schedule settlement asynchronously
|
|
383
|
+
import asyncio
|
|
384
|
+
|
|
385
|
+
# Get middleware reference (has facilitator)
|
|
386
|
+
d402_mw = getattr(request.state, 'd402_middleware', None)
|
|
387
|
+
|
|
388
|
+
# Use existing settle_payment function (no duplication!)
|
|
389
|
+
async def do_settlement():
|
|
390
|
+
try:
|
|
391
|
+
logger.info(f"🚀 Settlement task started for {func.__name__}")
|
|
392
|
+
logger.info(f" Tool result size: {len(str(result))} bytes")
|
|
393
|
+
|
|
394
|
+
# Create a simple wrapper context with payment_payload
|
|
395
|
+
class SettlementContext:
|
|
396
|
+
class State:
|
|
397
|
+
def __init__(self):
|
|
398
|
+
self.payment_payload = getattr(request.state, 'payment_payload', None)
|
|
399
|
+
self.payment_uuid = payment_uuid
|
|
400
|
+
self.facilitator_fee_percent = getattr(request.state, 'facilitator_fee_percent', 250)
|
|
401
|
+
|
|
402
|
+
def __init__(self):
|
|
403
|
+
self.state = SettlementContext.State()
|
|
404
|
+
|
|
405
|
+
settlement_ctx = SettlementContext()
|
|
406
|
+
|
|
407
|
+
# Call existing settle_payment function
|
|
408
|
+
settlement_success = await settle_payment(
|
|
409
|
+
context=settlement_ctx,
|
|
410
|
+
endpoint_info=endpoint_info,
|
|
411
|
+
output_data=result,
|
|
412
|
+
middleware=d402_mw # Pass Starlette middleware (has facilitator)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if settlement_success:
|
|
416
|
+
logger.info(f"✅ Settlement task completed successfully for {func.__name__}")
|
|
417
|
+
else:
|
|
418
|
+
logger.warning(f"⚠️ Settlement task failed for {func.__name__}")
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"❌ Settlement task error for {func.__name__}: {e}")
|
|
422
|
+
import traceback
|
|
423
|
+
logger.error(traceback.format_exc())
|
|
424
|
+
|
|
425
|
+
asyncio.create_task(do_settlement())
|
|
426
|
+
logger.info(f"📅 Settlement task scheduled for {func.__name__} - client gets response immediately")
|
|
427
|
+
|
|
428
|
+
# Return immediately - don't wait for settlement!
|
|
429
|
+
return result
|
|
430
|
+
|
|
431
|
+
return wrapper
|
|
432
|
+
return decorator
|
|
433
|
+
|
|
434
|
+
|