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.

Files changed (107) hide show
  1. traia_iatp/README.md +368 -0
  2. traia_iatp/__init__.py +54 -0
  3. traia_iatp/cli/__init__.py +5 -0
  4. traia_iatp/cli/main.py +483 -0
  5. traia_iatp/client/__init__.py +10 -0
  6. traia_iatp/client/a2a_client.py +274 -0
  7. traia_iatp/client/crewai_a2a_tools.py +335 -0
  8. traia_iatp/client/d402_a2a_client.py +293 -0
  9. traia_iatp/client/grpc_a2a_tools.py +349 -0
  10. traia_iatp/client/root_path_a2a_client.py +1 -0
  11. traia_iatp/contracts/__init__.py +12 -0
  12. traia_iatp/contracts/iatp_contracts_config.py +263 -0
  13. traia_iatp/contracts/wallet_creator.py +255 -0
  14. traia_iatp/core/__init__.py +43 -0
  15. traia_iatp/core/models.py +172 -0
  16. traia_iatp/d402/__init__.py +55 -0
  17. traia_iatp/d402/chains.py +102 -0
  18. traia_iatp/d402/client.py +150 -0
  19. traia_iatp/d402/clients/__init__.py +7 -0
  20. traia_iatp/d402/clients/base.py +218 -0
  21. traia_iatp/d402/clients/httpx.py +219 -0
  22. traia_iatp/d402/common.py +114 -0
  23. traia_iatp/d402/encoding.py +28 -0
  24. traia_iatp/d402/examples/client_example.py +197 -0
  25. traia_iatp/d402/examples/server_example.py +171 -0
  26. traia_iatp/d402/facilitator.py +453 -0
  27. traia_iatp/d402/fastapi_middleware/__init__.py +6 -0
  28. traia_iatp/d402/fastapi_middleware/middleware.py +225 -0
  29. traia_iatp/d402/fastmcp_middleware.py +147 -0
  30. traia_iatp/d402/mcp_middleware.py +434 -0
  31. traia_iatp/d402/middleware.py +193 -0
  32. traia_iatp/d402/models.py +116 -0
  33. traia_iatp/d402/networks.py +98 -0
  34. traia_iatp/d402/path.py +43 -0
  35. traia_iatp/d402/payment_introspection.py +104 -0
  36. traia_iatp/d402/payment_signing.py +178 -0
  37. traia_iatp/d402/paywall.py +119 -0
  38. traia_iatp/d402/starlette_middleware.py +326 -0
  39. traia_iatp/d402/template.py +1 -0
  40. traia_iatp/d402/types.py +300 -0
  41. traia_iatp/mcp/__init__.py +18 -0
  42. traia_iatp/mcp/client.py +201 -0
  43. traia_iatp/mcp/d402_mcp_tool_adapter.py +361 -0
  44. traia_iatp/mcp/mcp_agent_template.py +481 -0
  45. traia_iatp/mcp/templates/Dockerfile.j2 +80 -0
  46. traia_iatp/mcp/templates/README.md.j2 +310 -0
  47. traia_iatp/mcp/templates/cursor-rules.md.j2 +520 -0
  48. traia_iatp/mcp/templates/deployment_params.json.j2 +20 -0
  49. traia_iatp/mcp/templates/docker-compose.yml.j2 +32 -0
  50. traia_iatp/mcp/templates/dockerignore.j2 +47 -0
  51. traia_iatp/mcp/templates/env.example.j2 +57 -0
  52. traia_iatp/mcp/templates/gitignore.j2 +77 -0
  53. traia_iatp/mcp/templates/mcp_health_check.py.j2 +150 -0
  54. traia_iatp/mcp/templates/pyproject.toml.j2 +32 -0
  55. traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
  56. traia_iatp/mcp/templates/run_local_docker.sh.j2 +390 -0
  57. traia_iatp/mcp/templates/server.py.j2 +175 -0
  58. traia_iatp/mcp/traia_mcp_adapter.py +543 -0
  59. traia_iatp/preview_diagrams.html +181 -0
  60. traia_iatp/registry/__init__.py +26 -0
  61. traia_iatp/registry/atlas_search_indexes.json +280 -0
  62. traia_iatp/registry/embeddings.py +298 -0
  63. traia_iatp/registry/iatp_search_api.py +846 -0
  64. traia_iatp/registry/mongodb_registry.py +771 -0
  65. traia_iatp/registry/readmes/ATLAS_SEARCH_INDEXES.md +252 -0
  66. traia_iatp/registry/readmes/ATLAS_SEARCH_SETUP.md +134 -0
  67. traia_iatp/registry/readmes/AUTHENTICATION_UPDATE.md +124 -0
  68. traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +172 -0
  69. traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +257 -0
  70. traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +208 -0
  71. traia_iatp/registry/readmes/README.md +251 -0
  72. traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +191 -0
  73. traia_iatp/scripts/__init__.py +2 -0
  74. traia_iatp/scripts/create_wallet.py +244 -0
  75. traia_iatp/server/__init__.py +15 -0
  76. traia_iatp/server/a2a_server.py +219 -0
  77. traia_iatp/server/example_template_usage.py +72 -0
  78. traia_iatp/server/iatp_server_agent_generator.py +237 -0
  79. traia_iatp/server/iatp_server_template_generator.py +235 -0
  80. traia_iatp/server/templates/.dockerignore.j2 +48 -0
  81. traia_iatp/server/templates/Dockerfile.j2 +49 -0
  82. traia_iatp/server/templates/README.md +137 -0
  83. traia_iatp/server/templates/README.md.j2 +425 -0
  84. traia_iatp/server/templates/__init__.py +1 -0
  85. traia_iatp/server/templates/__main__.py.j2 +565 -0
  86. traia_iatp/server/templates/agent.py.j2 +94 -0
  87. traia_iatp/server/templates/agent_config.json.j2 +22 -0
  88. traia_iatp/server/templates/agent_executor.py.j2 +279 -0
  89. traia_iatp/server/templates/docker-compose.yml.j2 +23 -0
  90. traia_iatp/server/templates/env.example.j2 +84 -0
  91. traia_iatp/server/templates/gitignore.j2 +78 -0
  92. traia_iatp/server/templates/grpc_server.py.j2 +218 -0
  93. traia_iatp/server/templates/pyproject.toml.j2 +78 -0
  94. traia_iatp/server/templates/run_local_docker.sh.j2 +103 -0
  95. traia_iatp/server/templates/server.py.j2 +243 -0
  96. traia_iatp/special_agencies/__init__.py +4 -0
  97. traia_iatp/special_agencies/registry_search_agency.py +392 -0
  98. traia_iatp/utils/__init__.py +10 -0
  99. traia_iatp/utils/docker_utils.py +251 -0
  100. traia_iatp/utils/general.py +64 -0
  101. traia_iatp/utils/iatp_utils.py +126 -0
  102. traia_iatp-0.1.29.dist-info/METADATA +423 -0
  103. traia_iatp-0.1.29.dist-info/RECORD +107 -0
  104. traia_iatp-0.1.29.dist-info/WHEEL +5 -0
  105. traia_iatp-0.1.29.dist-info/entry_points.txt +2 -0
  106. traia_iatp-0.1.29.dist-info/licenses/LICENSE +21 -0
  107. 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
+