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,193 @@
1
+ """D402 middleware for IATP FastAPI servers (utility agents)."""
2
+
3
+ import logging
4
+ from typing import Callable, Optional
5
+ from fastapi import Request
6
+ from fastapi.responses import JSONResponse, HTMLResponse
7
+
8
+ from .fastapi_middleware.middleware import require_payment as d402_require_payment
9
+ from .types import Price, PaywallConfig, HTTPInputSchema
10
+ from typing import Callable, Dict
11
+ from typing_extensions import TypedDict
12
+
13
+ from .models import D402Config, PaymentScheme
14
+
15
+ # FacilitatorConfig (copied from d402)
16
+ class FacilitatorConfig(TypedDict, total=False):
17
+ url: str
18
+ create_headers: Callable[[], dict[str, dict[str, str]]]
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class D402IATPMiddleware:
24
+ """Middleware that integrates d402 payments into IATP servers.
25
+
26
+ This middleware wraps the Coinbase d402 middleware and adapts it for
27
+ IATP utility agents, connecting to the custom IATP Settlement Layer.
28
+ """
29
+
30
+ def __init__(self, config: D402Config):
31
+ """Initialize the d402 IATP middleware.
32
+
33
+ Args:
34
+ config: D402 configuration including pricing and facilitator settings
35
+ """
36
+ self.config = config
37
+ self.facilitator_config: FacilitatorConfig = {
38
+ "url": config.facilitator_url,
39
+ }
40
+
41
+ # Add authentication headers if API key is provided
42
+ if config.facilitator_api_key:
43
+ async def create_headers():
44
+ return {
45
+ "verify": {"Authorization": f"Bearer {config.facilitator_api_key}"},
46
+ "settle": {"Authorization": f"Bearer {config.facilitator_api_key}"}
47
+ }
48
+ self.facilitator_config["create_headers"] = create_headers
49
+
50
+ def create_middleware(
51
+ self,
52
+ skill_id: Optional[str] = None,
53
+ custom_price: Optional[Price] = None,
54
+ custom_description: Optional[str] = None,
55
+ ) -> Callable:
56
+ """Create a FastAPI middleware function for a specific skill or endpoint.
57
+
58
+ Args:
59
+ skill_id: Optional skill ID to use custom pricing
60
+ custom_price: Optional override price
61
+ custom_description: Optional override description
62
+
63
+ Returns:
64
+ FastAPI middleware function
65
+ """
66
+ if not self.config.enabled:
67
+ # Return passthrough middleware if payments not enabled
68
+ async def passthrough(request: Request, call_next: Callable):
69
+ return await call_next(request)
70
+ return passthrough
71
+
72
+ # Determine the price to use
73
+ if custom_price:
74
+ price = custom_price
75
+ elif skill_id and skill_id in self.config.skill_prices:
76
+ price_config = self.config.skill_prices[skill_id]
77
+ price = price_config.usd_amount
78
+ else:
79
+ price = self.config.default_price.usd_amount
80
+
81
+ # Determine the description
82
+ description = custom_description or self.config.service_description
83
+
84
+ # Get the pricing configuration
85
+ price_config = (
86
+ self.config.skill_prices.get(skill_id)
87
+ if skill_id and skill_id in self.config.skill_prices
88
+ else self.config.default_price
89
+ )
90
+
91
+ # Create the d402 middleware using Coinbase's implementation
92
+ return d402_require_payment(
93
+ price=price,
94
+ pay_to_address=self.config.pay_to_address,
95
+ path=self.config.protected_paths,
96
+ description=description,
97
+ mime_type="application/json",
98
+ max_deadline_seconds=price_config.max_timeout_seconds,
99
+ input_schema=HTTPInputSchema(
100
+ query_params=None,
101
+ body_type="json",
102
+ body_fields=None,
103
+ header_fields=None
104
+ ),
105
+ output_schema=None,
106
+ discoverable=True,
107
+ facilitator_config=self.facilitator_config,
108
+ network=price_config.network,
109
+ resource=None, # Will be determined from request URL
110
+ paywall_config=None, # Could add custom paywall UI here
111
+ custom_paywall_html=None
112
+ )
113
+
114
+
115
+ def require_iatp_payment(
116
+ config: D402Config,
117
+ skill_id: Optional[str] = None,
118
+ custom_price: Optional[Price] = None,
119
+ custom_description: Optional[str] = None,
120
+ ) -> Callable:
121
+ """Convenience function to create d402 middleware for IATP.
122
+
123
+ Usage:
124
+ @app.middleware("http")
125
+ async def payment_middleware(request: Request, call_next):
126
+ middleware = require_iatp_payment(d402_config)
127
+ return await middleware(request, call_next)
128
+
129
+ Or for specific routes:
130
+ @app.post("/process")
131
+ async def process_request(request: Request):
132
+ # Will require payment
133
+ pass
134
+
135
+ app.middleware("http")(
136
+ require_iatp_payment(d402_config, skill_id="process_request")
137
+ )
138
+
139
+ Args:
140
+ config: D402 configuration
141
+ skill_id: Optional skill ID for custom pricing
142
+ custom_price: Optional price override
143
+ custom_description: Optional description override
144
+
145
+ Returns:
146
+ FastAPI middleware function
147
+ """
148
+ middleware = D402IATPMiddleware(config)
149
+ return middleware.create_middleware(skill_id, custom_price, custom_description)
150
+
151
+
152
+ async def add_d402_info_to_agent_card(agent_card: dict, config: D402Config) -> dict:
153
+ """Add d402 payment information to an agent card.
154
+
155
+ This adds payment capabilities to the agent card for client discovery.
156
+
157
+ Args:
158
+ agent_card: The agent card dictionary
159
+ config: D402 configuration
160
+
161
+ Returns:
162
+ Updated agent card with payment information
163
+ """
164
+ if not config.enabled:
165
+ return agent_card
166
+
167
+ # Add d402 payment information to metadata
168
+ agent_card.setdefault("metadata", {})
169
+ agent_card["metadata"]["d402"] = {
170
+ "enabled": True,
171
+ "paymentSchemes": [PaymentScheme.EXACT.value],
172
+ "networks": [config.default_price.network],
173
+ "defaultPrice": {
174
+ "usdAmount": config.default_price.usd_amount,
175
+ "network": config.default_price.network,
176
+ "asset": config.default_price.asset_address,
177
+ "maxTimeoutSeconds": config.default_price.max_timeout_seconds
178
+ },
179
+ "payToAddress": config.pay_to_address,
180
+ "facilitatorUrl": config.facilitator_url,
181
+ "skillPrices": {
182
+ skill_id: {
183
+ "usdAmount": price.usd_amount,
184
+ "network": price.network,
185
+ "asset": price.asset_address,
186
+ "maxTimeoutSeconds": price.max_timeout_seconds
187
+ }
188
+ for skill_id, price in config.skill_prices.items()
189
+ }
190
+ }
191
+
192
+ return agent_card
193
+
@@ -0,0 +1,116 @@
1
+ """D402 payment models for IATP protocol."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional, Dict, Any
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class PaymentScheme(str, Enum):
9
+ """Payment schemes supported by d402."""
10
+ EXACT = "exact" # EIP-3009 exact payment
11
+
12
+
13
+ class D402ServicePrice(BaseModel):
14
+ """Pricing configuration for an IATP service.
15
+
16
+ Supports any ERC20 token payment with full token details.
17
+ """
18
+
19
+ # Token details
20
+ token_address: str = Field(..., description="Token contract address (e.g., USDC, TRAIA, DAI)")
21
+ token_symbol: str = Field(..., description="Token symbol for display")
22
+ token_decimals: int = Field(..., description="Token decimals (6 for USDC, 18 for most)")
23
+
24
+ # Price (stored in multiple formats for convenience)
25
+ price_wei: str = Field(..., description="Price in wei/atomic units")
26
+ price_float: float = Field(..., description="Price in token units (human-readable)")
27
+
28
+ # Network configuration
29
+ network: str = Field(..., description="Network (sepolia, base-sepolia, etc.)")
30
+ chain_id: int = Field(..., description="Chain ID")
31
+
32
+ # Optional USD equivalent (for display only)
33
+ usd_amount: Optional[float] = Field(None, description="Approximate USD value")
34
+
35
+ # Maximum timeout for payment completion
36
+ max_timeout_seconds: int = Field(default=300, description="Max time to complete payment")
37
+
38
+
39
+ class D402Config(BaseModel):
40
+ """Configuration for d402 payment integration in IATP.
41
+
42
+ Supports per-path pricing with different tokens.
43
+ """
44
+
45
+ # Enable/disable d402 payments
46
+ enabled: bool = Field(default=False, description="Enable d402 payments")
47
+
48
+ # Payment address (utility agent contract address)
49
+ pay_to_address: str = Field(..., description="Ethereum address to receive payments")
50
+
51
+ # Pricing configuration
52
+ # Can be per-path (e.g., {"/analyze": D402ServicePrice(...), "/extract": D402ServicePrice(...)})
53
+ # or default for all paths
54
+ path_prices: Dict[str, D402ServicePrice] = Field(
55
+ default_factory=dict,
56
+ description="Price configuration per path"
57
+ )
58
+
59
+ # Default price (used if path not in path_prices)
60
+ default_price: Optional[D402ServicePrice] = Field(None, description="Default price for all paths")
61
+
62
+ # Legacy: Pricing per service/skill (deprecated, use path_prices)
63
+ skill_prices: Dict[str, D402ServicePrice] = Field(
64
+ default_factory=dict,
65
+ description="Custom prices per skill ID (deprecated)"
66
+ )
67
+
68
+ # Facilitator configuration
69
+ facilitator_url: str = Field(
70
+ default="https://api.traia.io/d402/facilitator",
71
+ description="URL of the d402 facilitator service"
72
+ )
73
+
74
+ # Custom facilitator authentication (if needed)
75
+ facilitator_api_key: Optional[str] = Field(None, description="API key for facilitator")
76
+
77
+ # Paths to gate with payments (* for all)
78
+ protected_paths: list[str] = Field(
79
+ default_factory=lambda: ["*"],
80
+ description="Paths that require payment"
81
+ )
82
+
83
+ # Service description for payment prompt
84
+ service_description: str = Field(..., description="Description shown in payment UI")
85
+
86
+ # Metadata
87
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
88
+
89
+
90
+ class D402PaymentInfo(BaseModel):
91
+ """Payment information for agent card discovery."""
92
+
93
+ enabled: bool = Field(..., description="Whether d402 is enabled")
94
+ payment_schemes: list[PaymentScheme] = Field(
95
+ default_factory=lambda: [PaymentScheme.EXACT],
96
+ description="Supported payment schemes"
97
+ )
98
+ networks: list[str] = Field(..., description="Supported blockchain networks")
99
+ default_price: D402ServicePrice = Field(..., description="Default pricing")
100
+ facilitator_url: str = Field(..., description="Facilitator service URL")
101
+
102
+ class Config:
103
+ use_enum_values = True
104
+
105
+
106
+ class IATPSettlementRequest(BaseModel):
107
+ """Request to settle a payment through IATP settlement layer."""
108
+
109
+ consumer: str = Field(..., description="Consumer address (client agent)")
110
+ provider: str = Field(..., description="Provider address (utility agent)")
111
+ amount: str = Field(..., description="Amount in atomic units")
112
+ timestamp: int = Field(..., description="Request timestamp")
113
+ service_description: str = Field(..., description="Description of service")
114
+ consumer_signature: str = Field(..., description="Consumer's EIP-712 signature")
115
+ provider_signature: str = Field(..., description="Provider's attestation signature")
116
+
@@ -0,0 +1,98 @@
1
+ """Network configuration for d402 payments.
2
+
3
+ This module defines supported networks and their token configurations.
4
+ Customized for IATP - uses database-driven network configuration.
5
+ """
6
+
7
+ from typing import Literal, Dict, Any
8
+ from typing_extensions import TypedDict
9
+
10
+
11
+ # Network type definition
12
+ SupportedNetworks = Literal[
13
+ "sepolia",
14
+ "base-sepolia",
15
+ "arbitrum-sepolia",
16
+ "base-mainnet",
17
+ "arbitrum-mainnet",
18
+ ]
19
+
20
+
21
+ class NetworkConfig(TypedDict):
22
+ """Network configuration."""
23
+ chain_id: int
24
+ name: str
25
+ rpc_url: str
26
+ explorer_url: str
27
+ usdc_address: str
28
+
29
+
30
+ # Network configurations
31
+ NETWORKS: Dict[str, NetworkConfig] = {
32
+ "sepolia": {
33
+ "chain_id": 11155111,
34
+ "name": "Ethereum Sepolia",
35
+ "rpc_url": "https://ethereum-sepolia-rpc.publicnode.com",
36
+ "explorer_url": "https://sepolia.etherscan.io",
37
+ "usdc_address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"
38
+ },
39
+ "base-sepolia": {
40
+ "chain_id": 84532,
41
+ "name": "Base Sepolia",
42
+ "rpc_url": "https://sepolia.base.org",
43
+ "explorer_url": "https://sepolia.basescan.org",
44
+ "usdc_address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
45
+ },
46
+ "arbitrum-sepolia": {
47
+ "chain_id": 421614,
48
+ "name": "Arbitrum Sepolia",
49
+ "rpc_url": "https://arbitrum-sepolia-rpc.publicnode.com",
50
+ "explorer_url": "https://sepolia.arbiscan.io",
51
+ "usdc_address": "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d"
52
+ },
53
+ "base-mainnet": {
54
+ "chain_id": 8453,
55
+ "name": "Base",
56
+ "rpc_url": "https://mainnet.base.org",
57
+ "explorer_url": "https://basescan.org",
58
+ "usdc_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
59
+ },
60
+ "arbitrum-mainnet": {
61
+ "chain_id": 42161,
62
+ "name": "Arbitrum One",
63
+ "rpc_url": "https://arbitrum-one-rpc.publicnode.com",
64
+ "explorer_url": "https://arbiscan.io",
65
+ "usdc_address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
66
+ }
67
+ }
68
+
69
+
70
+ def get_network_config(network: str) -> NetworkConfig:
71
+ """Get configuration for a network."""
72
+ if network not in NETWORKS:
73
+ raise ValueError(f"Unsupported network: {network}")
74
+ return NETWORKS[network]
75
+
76
+
77
+ def get_usdc_address(network: str) -> str:
78
+ """Get USDC address for a network."""
79
+ return get_network_config(network)["usdc_address"]
80
+
81
+
82
+ def get_chain_id(network: str) -> int:
83
+ """Get chain ID for a network."""
84
+ return get_network_config(network)["chain_id"]
85
+
86
+
87
+ # TODO: Load from database network table
88
+ async def load_networks_from_db() -> Dict[str, NetworkConfig]:
89
+ """Load network configurations from database.
90
+
91
+ This will query the Network table and build NETWORKS dict dynamically.
92
+ For now, returns static config.
93
+ """
94
+ # from db.dal.models import Network
95
+ # networks = await db.query(Network).filter(Network.active == True).all()
96
+ # return {net.shortname: {...} for net in networks}
97
+ return NETWORKS
98
+
@@ -0,0 +1,43 @@
1
+ import fnmatch
2
+ import re
3
+ from typing import Union
4
+
5
+
6
+ def path_is_match(path: Union[str, list[str]], request_path: str) -> bool:
7
+ """
8
+ Check if request path matches the specified path pattern(s).
9
+
10
+ Supports:
11
+ - Exact matching: "/api/users"
12
+ - Glob patterns: "/api/users/*", "/api/*/profile"
13
+ - Regex patterns (prefix with 'regex:'): "regex:^/api/users/\\d+$"
14
+ - List of any of the above
15
+
16
+ Args:
17
+ path: Path pattern(s) to match against. Can be a string or list of strings.
18
+ request_path: The actual request path to check.
19
+
20
+ Returns:
21
+ bool: True if the request path matches any of the patterns, False otherwise.
22
+ """
23
+
24
+ def single_path_match(pattern: str) -> bool:
25
+ # Regex pattern
26
+ if pattern.startswith("regex:"):
27
+ regex_pattern = pattern[6:] # Remove 'regex:' prefix
28
+ return bool(re.match(regex_pattern, request_path))
29
+
30
+ # Glob pattern (contains * or ?)
31
+ elif "*" in pattern or "?" in pattern:
32
+ return fnmatch.fnmatch(request_path, pattern)
33
+
34
+ # Exact match
35
+ else:
36
+ return pattern == request_path
37
+
38
+ if isinstance(path, str):
39
+ return single_path_match(path)
40
+ elif isinstance(path, list):
41
+ return any(single_path_match(p) for p in path)
42
+
43
+ return False
@@ -0,0 +1,104 @@
1
+ """
2
+ Helper to extract payment configurations from @require_payment_for_tool decorators.
3
+
4
+ This allows us to have a single source of truth - payment config is declared
5
+ in the decorator, and we introspect it to build TOOL_PAYMENT_CONFIGS.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Any, Optional
10
+
11
+ from .types import TokenAmount
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def extract_payment_configs_from_mcp(mcp_server, server_address: str) -> Dict[str, Dict[str, Any]]:
17
+ """
18
+ Extract payment configurations from tools decorated with @require_payment_for_tool.
19
+
20
+ This introspects the decorator closures to extract TokenAmount objects,
21
+ eliminating the need to duplicate payment configuration.
22
+
23
+ Args:
24
+ mcp_server: FastMCP server instance
25
+ server_address: Server's payment address
26
+
27
+ Returns:
28
+ Dict mapping tool names to payment configurations
29
+ Format: {"tool_name": {"price_wei": "1000", "token_address": "0x...", ...}}
30
+
31
+ Usage:
32
+ mcp = FastMCP("Server")
33
+
34
+ # Add tools with @require_payment_for_tool decorators
35
+ @mcp.tool()
36
+ @require_payment_for_tool(price=TokenAmount(...))
37
+ async def my_tool(context): ...
38
+
39
+ # Extract configs dynamically
40
+ TOOL_PAYMENT_CONFIGS = extract_payment_configs_from_mcp(mcp, SERVER_ADDRESS)
41
+
42
+ # Add middleware with extracted configs
43
+ app.add_middleware(D402PaymentMiddleware, tool_payment_configs=TOOL_PAYMENT_CONFIGS, ...)
44
+ """
45
+ tool_payment_configs = {}
46
+
47
+ try:
48
+ # Get registered tools from FastMCP
49
+ tools = mcp_server._tool_manager.list_tools()
50
+
51
+ for tool in tools:
52
+ if not hasattr(tool, 'fn'):
53
+ continue
54
+
55
+ fn = tool.fn
56
+ tool_name = tool.name
57
+
58
+ # Check if function has a closure (from decorators)
59
+ if not hasattr(fn, '__closure__') or not fn.__closure__:
60
+ logger.debug(f"Tool {tool_name}: No closure (no payment decorator)")
61
+ continue
62
+
63
+ # Look for TokenAmount in closure
64
+ token_amount = None
65
+ for cell in fn.__closure__:
66
+ try:
67
+ val = cell.cell_contents
68
+ if isinstance(val, TokenAmount):
69
+ token_amount = val
70
+ break
71
+ except:
72
+ pass
73
+
74
+ if token_amount:
75
+ # Extract payment config from TokenAmount (including EIP712 domain)
76
+ config = {
77
+ "price_wei": token_amount.amount,
78
+ "token_address": token_amount.asset.address,
79
+ "network": token_amount.asset.network,
80
+ "server_address": server_address,
81
+ "description": tool.description or tool_name,
82
+ "eip712_domain": {
83
+ "name": token_amount.asset.eip712.name if token_amount.asset.eip712 else "USD Coin",
84
+ "version": token_amount.asset.eip712.version if token_amount.asset.eip712 else "2"
85
+ }
86
+ }
87
+
88
+ tool_payment_configs[tool_name] = config
89
+ logger.info(f"✅ Extracted payment config for {tool_name}: {config['price_wei']} wei on {config['network']}")
90
+ else:
91
+ logger.debug(f"Tool {tool_name}: No TokenAmount found (free tool)")
92
+
93
+ logger.info(f"📊 Extracted {len(tool_payment_configs)} payment configs from decorators")
94
+
95
+ except Exception as e:
96
+ logger.error(f"Error extracting payment configs: {e}")
97
+ import traceback
98
+ logger.error(traceback.format_exc())
99
+
100
+ return tool_payment_configs
101
+
102
+
103
+ __all__ = ["extract_payment_configs_from_mcp"]
104
+