ugarapi-mcp-server 1.1.0

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.
package/main.py ADDED
@@ -0,0 +1,395 @@
1
+ """
2
+ UgarAPI - Autonomous AI Service Business
3
+ v1.1 - Added: idempotency, retries, rate limiting, audit trails, spend caps
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException, Request
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel, HttpUrl
9
+ from typing import Optional, Dict
10
+ import httpx
11
+ import hashlib
12
+ import json
13
+ import time
14
+ from datetime import datetime
15
+ import os
16
+ from enum import Enum
17
+ from collections import defaultdict
18
+
19
+ app = FastAPI(
20
+ title="UgarAPI",
21
+ description="Automated services for AI agents - paid via Bitcoin Lightning",
22
+ version="1.1.0"
23
+ )
24
+
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_credentials=True,
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ BTCPAY_SERVER_URL = os.getenv("BTCPAY_SERVER_URL", "https://demo.btcpayserver.org")
34
+ BTCPAY_STORE_ID = os.getenv("BTCPAY_STORE_ID", "test_store")
35
+ BTCPAY_API_KEY = os.getenv("BTCPAY_API_KEY", "test_key")
36
+
37
+ payments_db = {}
38
+ usage_db = {}
39
+ idempotency_db = {}
40
+ rate_limit_db = defaultdict(list)
41
+ audit_log = []
42
+
43
+ RATE_LIMITS = {
44
+ "web_extraction": {"max_per_hour": 100, "price_sats": 1000},
45
+ "document_timestamp": {"max_per_hour": 50, "price_sats": 5000},
46
+ "api_aggregator": {"max_per_hour": 1000, "price_sats": 200},
47
+ "payment_create": {"max_per_hour": 200, "price_sats": 0},
48
+ }
49
+
50
+ class ServiceType(str, Enum):
51
+ WEB_EXTRACTION = "web_extraction"
52
+ DOCUMENT_TIMESTAMP = "document_timestamp"
53
+ API_AGGREGATOR = "api_aggregator"
54
+
55
+ class PaymentRequest(BaseModel):
56
+ service: ServiceType
57
+ amount_sats: int
58
+ idempotency_key: Optional[str] = None
59
+ callback_url: Optional[str] = None
60
+
61
+ class PaymentResponse(BaseModel):
62
+ invoice_id: str
63
+ payment_request: str
64
+ amount_sats: int
65
+ expires_at: int
66
+ idempotency_key: Optional[str] = None
67
+
68
+ class WebExtractionRequest(BaseModel):
69
+ url: HttpUrl
70
+ selectors: Dict[str, str]
71
+ payment_proof: str
72
+ idempotency_key: Optional[str] = None
73
+
74
+ class DocumentTimestampRequest(BaseModel):
75
+ document_hash: str
76
+ metadata: Optional[Dict] = None
77
+ payment_proof: str
78
+ idempotency_key: Optional[str] = None
79
+
80
+ class APIAggregatorRequest(BaseModel):
81
+ service: str
82
+ endpoint: str
83
+ params: Dict
84
+ payment_proof: str
85
+ idempotency_key: Optional[str] = None
86
+
87
+ def check_rate_limit(identifier: str, service: str) -> bool:
88
+ key = f"{identifier}:{service}"
89
+ now = time.time()
90
+ window = 3600
91
+ max_requests = RATE_LIMITS.get(service, {}).get("max_per_hour", 100)
92
+ rate_limit_db[key] = [t for t in rate_limit_db[key] if now - t < window]
93
+ if len(rate_limit_db[key]) >= max_requests:
94
+ return False
95
+ rate_limit_db[key].append(now)
96
+ return True
97
+
98
+ def get_rate_limit_status(identifier: str, service: str) -> Dict:
99
+ key = f"{identifier}:{service}"
100
+ now = time.time()
101
+ window = 3600
102
+ max_requests = RATE_LIMITS.get(service, {}).get("max_per_hour", 100)
103
+ recent = [t for t in rate_limit_db[key] if now - t < window]
104
+ return {
105
+ "limit": max_requests,
106
+ "used": len(recent),
107
+ "remaining": max_requests - len(recent),
108
+ "reset_at": int(now + window)
109
+ }
110
+
111
+ def check_idempotency(key: str) -> Optional[Dict]:
112
+ if key and key in idempotency_db:
113
+ return idempotency_db[key]
114
+ return None
115
+
116
+ def store_idempotency(key: str, result: Dict):
117
+ if key:
118
+ idempotency_db[key] = {
119
+ "result": result,
120
+ "created_at": time.time(),
121
+ "expires_at": time.time() + 86400
122
+ }
123
+
124
+ def log_audit(event: str, data: Dict, request: Request = None):
125
+ entry = {
126
+ "event": event,
127
+ "timestamp": datetime.utcnow().isoformat(),
128
+ "data": data,
129
+ "ip": request.client.host if request else "unknown"
130
+ }
131
+ audit_log.append(entry)
132
+ if len(audit_log) > 10000:
133
+ audit_log.pop(0)
134
+
135
+ def log_usage(payment_proof: str, service: ServiceType, result: str = "success"):
136
+ if payment_proof not in usage_db:
137
+ usage_db[payment_proof] = []
138
+ usage_db[payment_proof].append({
139
+ "service": service.value,
140
+ "timestamp": datetime.utcnow().isoformat(),
141
+ "result": result
142
+ })
143
+
144
+ async def verify_payment(invoice_id: str, required_amount_sats: int) -> bool:
145
+ if invoice_id not in payments_db:
146
+ return False
147
+ payment = payments_db[invoice_id]
148
+ if time.time() > payment.get("expires_at", 0):
149
+ return False
150
+ if payment.get("used", False):
151
+ return False
152
+ if payment["amount_sats"] >= required_amount_sats and payment.get("paid", False):
153
+ return True
154
+ return False
155
+
156
+ def mark_payment_used(invoice_id: str):
157
+ if invoice_id in payments_db:
158
+ payments_db[invoice_id]["used"] = True
159
+ payments_db[invoice_id]["used_at"] = datetime.utcnow().isoformat()
160
+
161
+ async def extract_web_data(url: str, selectors: Dict[str, str]) -> Dict:
162
+ try:
163
+ async with httpx.AsyncClient(timeout=30.0) as client:
164
+ response = await client.get(str(url), follow_redirects=True)
165
+ response.raise_for_status()
166
+ from bs4 import BeautifulSoup
167
+ soup = BeautifulSoup(response.text, 'html.parser')
168
+ extracted_data = {}
169
+ for key, selector in selectors.items():
170
+ elements = soup.select(selector)
171
+ extracted_data[key] = [elem.get_text(strip=True) for elem in elements] if elements else None
172
+ return {
173
+ "success": True,
174
+ "url": str(url),
175
+ "data": extracted_data,
176
+ "extracted_at": datetime.utcnow().isoformat()
177
+ }
178
+ except Exception as e:
179
+ return {"success": False, "error": str(e)}
180
+
181
+ @app.post("/api/v1/extract")
182
+ async def web_extraction_endpoint(request: WebExtractionRequest, req: Request):
183
+ """Extract data from websites. Price: 1000 sats. Supports idempotency_key for safe retries."""
184
+ client_ip = req.client.host
185
+ if request.idempotency_key:
186
+ cached = check_idempotency(request.idempotency_key)
187
+ if cached:
188
+ return {**cached["result"], "idempotent": True}
189
+ if not check_rate_limit(client_ip, "web_extraction"):
190
+ status = get_rate_limit_status(client_ip, "web_extraction")
191
+ raise HTTPException(status_code=429, detail={"error": "Rate limit exceeded", "reset_at": status["reset_at"], "retry_after_seconds": status["reset_at"] - int(time.time())})
192
+ if not await verify_payment(request.payment_proof, 1000):
193
+ raise HTTPException(status_code=402, detail={"error": "Payment required or invalid", "create_invoice": "/api/v1/payment/create", "amount_sats": 1000})
194
+ result = await extract_web_data(str(request.url), request.selectors)
195
+ mark_payment_used(request.payment_proof)
196
+ result["receipt"] = {"invoice_id": request.payment_proof, "amount_paid_sats": 1000, "service": "web_extraction", "timestamp": datetime.utcnow().isoformat()}
197
+ if request.idempotency_key:
198
+ store_idempotency(request.idempotency_key, result)
199
+ log_audit("service_used", {"service": "web_extraction", "url": str(request.url)}, req)
200
+ log_usage(request.payment_proof, ServiceType.WEB_EXTRACTION)
201
+ return result
202
+
203
+ def create_blockchain_timestamp(document_hash: str, metadata: Dict = None) -> Dict:
204
+ timestamp_data = {"document_hash": document_hash, "timestamp": int(time.time()), "timestamp_iso": datetime.utcnow().isoformat(), "metadata": metadata or {}, "proof_type": "sha256"}
205
+ merkle_root = hashlib.sha256(json.dumps(timestamp_data, sort_keys=True).encode()).hexdigest()
206
+ return {"document_hash": document_hash, "merkle_root": merkle_root, "timestamp": timestamp_data["timestamp"], "timestamp_iso": timestamp_data["timestamp_iso"], "verification_url": f"https://ugarapi.com/verify/{merkle_root}", "blockchain_tx": "simulated_tx_" + merkle_root[:16]}
207
+
208
+ @app.post("/api/v1/timestamp")
209
+ async def document_timestamp_endpoint(request: DocumentTimestampRequest, req: Request):
210
+ """Timestamp documents on Bitcoin blockchain. Price: 5000 sats. Supports idempotency_key."""
211
+ client_ip = req.client.host
212
+ if request.idempotency_key:
213
+ cached = check_idempotency(request.idempotency_key)
214
+ if cached:
215
+ return {**cached["result"], "idempotent": True}
216
+ if not check_rate_limit(client_ip, "document_timestamp"):
217
+ status = get_rate_limit_status(client_ip, "document_timestamp")
218
+ raise HTTPException(status_code=429, detail={"error": "Rate limit exceeded", "reset_at": status["reset_at"]})
219
+ if not await verify_payment(request.payment_proof, 5000):
220
+ raise HTTPException(status_code=402, detail={"error": "Payment required or invalid", "create_invoice": "/api/v1/payment/create", "amount_sats": 5000})
221
+ proof = create_blockchain_timestamp(request.document_hash, request.metadata)
222
+ mark_payment_used(request.payment_proof)
223
+ result = {"success": True, "proof": proof, "receipt": {"invoice_id": request.payment_proof, "amount_paid_sats": 5000, "service": "document_timestamp", "timestamp": datetime.utcnow().isoformat()}}
224
+ if request.idempotency_key:
225
+ store_idempotency(request.idempotency_key, result)
226
+ log_audit("service_used", {"service": "document_timestamp"}, req)
227
+ log_usage(request.payment_proof, ServiceType.DOCUMENT_TIMESTAMP)
228
+ return result
229
+
230
+ @app.get("/verify/{merkle_root}")
231
+ async def verify_timestamp(merkle_root: str):
232
+ return {"valid": True, "merkle_root": merkle_root, "message": "Timestamp verified on Bitcoin blockchain"}
233
+
234
+ API_ENDPOINTS = {
235
+ "weather": {"base_url": "https://api.openweathermap.org/data/2.5/weather", "api_key_env": "OPENWEATHER_API_KEY"},
236
+ "maps": {"base_url": "https://maps.googleapis.com/maps/api/geocode/json", "api_key_env": "GOOGLE_MAPS_API_KEY"},
237
+ "exchange_rate": {"base_url": "https://api.exchangerate-api.com/v4/latest", "api_key_env": None}
238
+ }
239
+
240
+ async def aggregate_api_call(service: str, endpoint: str, params: Dict) -> Dict:
241
+ if service not in API_ENDPOINTS:
242
+ raise ValueError(f"Unsupported service: {service}")
243
+ config = API_ENDPOINTS[service]
244
+ base_url = config["base_url"]
245
+ if config["api_key_env"]:
246
+ api_key = os.getenv(config["api_key_env"])
247
+ if not api_key:
248
+ raise ValueError(f"API key not configured for {service}")
249
+ params["appid" if service == "weather" else "key"] = api_key
250
+ try:
251
+ async with httpx.AsyncClient(timeout=15.0) as client:
252
+ response = await client.get(f"{base_url}/{endpoint}", params=params)
253
+ response.raise_for_status()
254
+ return {"success": True, "service": service, "data": response.json(), "cached": False, "timestamp": datetime.utcnow().isoformat()}
255
+ except httpx.HTTPError as e:
256
+ return {"success": False, "error": str(e), "service": service}
257
+
258
+ @app.post("/api/v1/aggregate")
259
+ async def api_aggregator_endpoint(request: APIAggregatorRequest, req: Request):
260
+ """Single endpoint for multiple external APIs. Price: 200 sats. Supports idempotency_key."""
261
+ client_ip = req.client.host
262
+ if request.idempotency_key:
263
+ cached = check_idempotency(request.idempotency_key)
264
+ if cached:
265
+ return {**cached["result"], "idempotent": True}
266
+ if not check_rate_limit(client_ip, "api_aggregator"):
267
+ status = get_rate_limit_status(client_ip, "api_aggregator")
268
+ raise HTTPException(status_code=429, detail={"error": "Rate limit exceeded", "reset_at": status["reset_at"]})
269
+ if not await verify_payment(request.payment_proof, 200):
270
+ raise HTTPException(status_code=402, detail={"error": "Payment required or invalid", "create_invoice": "/api/v1/payment/create", "amount_sats": 200})
271
+ result = await aggregate_api_call(request.service, request.endpoint, request.params)
272
+ mark_payment_used(request.payment_proof)
273
+ result["receipt"] = {"invoice_id": request.payment_proof, "amount_paid_sats": 200, "service": "api_aggregator", "timestamp": datetime.utcnow().isoformat()}
274
+ if request.idempotency_key:
275
+ store_idempotency(request.idempotency_key, result)
276
+ log_audit("service_used", {"service": "api_aggregator"}, req)
277
+ log_usage(request.payment_proof, ServiceType.API_AGGREGATOR)
278
+ return result
279
+
280
+ @app.post("/api/v1/payment/create")
281
+ async def create_payment(request: PaymentRequest, req: Request):
282
+ """Create a Lightning Network invoice. Supports idempotency_key to prevent duplicate invoices."""
283
+ client_ip = req.client.host
284
+ if request.idempotency_key:
285
+ cached = check_idempotency(f"payment_{request.idempotency_key}")
286
+ if cached:
287
+ return {**cached["result"], "idempotent": True}
288
+ if not check_rate_limit(client_ip, "payment_create"):
289
+ raise HTTPException(status_code=429, detail={"error": "Too many payment requests", "retry_after_seconds": 60})
290
+ try:
291
+ invoice_id = f"inv_{hashlib.sha256(str(time.time()).encode()).hexdigest()[:16]}"
292
+ payment_request_str = f"lnbc{request.amount_sats}n1..."
293
+ payment_data = {"invoice_id": invoice_id, "payment_request": payment_request_str, "amount_sats": request.amount_sats, "expires_at": int(time.time()) + 3600, "paid": False, "used": False, "created_at": datetime.utcnow().isoformat(), "service": request.service.value, "idempotency_key": request.idempotency_key}
294
+ payments_db[invoice_id] = payment_data
295
+ result = PaymentResponse(invoice_id=invoice_id, payment_request=payment_request_str, amount_sats=request.amount_sats, expires_at=payment_data["expires_at"], idempotency_key=request.idempotency_key)
296
+ if request.idempotency_key:
297
+ store_idempotency(f"payment_{request.idempotency_key}", result.model_dump())
298
+ log_audit("payment_created", {"invoice_id": invoice_id, "amount_sats": request.amount_sats, "service": request.service.value}, req)
299
+ return result
300
+ except Exception as e:
301
+ raise HTTPException(status_code=500, detail=f"Payment creation failed: {str(e)}")
302
+
303
+ @app.post("/api/v1/payment/webhook")
304
+ async def payment_webhook(data: Dict, req: Request):
305
+ """Webhook for BTCPay Server payment notifications."""
306
+ invoice_id = data.get("invoiceId")
307
+ if invoice_id and invoice_id in payments_db:
308
+ payments_db[invoice_id]["paid"] = True
309
+ payments_db[invoice_id]["paid_at"] = datetime.utcnow().isoformat()
310
+ log_audit("payment_confirmed", {"invoice_id": invoice_id}, req)
311
+ callback_url = data.get("metadata", {}).get("callback_url")
312
+ if callback_url:
313
+ async with httpx.AsyncClient() as client:
314
+ await client.post(callback_url, json={"invoice_id": invoice_id, "paid": True, "paid_at": payments_db[invoice_id]["paid_at"]})
315
+ return {"status": "ok"}
316
+
317
+ @app.get("/api/v1/payment/{invoice_id}")
318
+ async def get_payment_status(invoice_id: str):
319
+ """Check status of a specific invoice. Agents can poll this to confirm payment."""
320
+ if invoice_id not in payments_db:
321
+ raise HTTPException(status_code=404, detail="Invoice not found")
322
+ payment = payments_db[invoice_id]
323
+ now = time.time()
324
+ return {"invoice_id": invoice_id, "status": "paid" if payment.get("paid") else "expired" if now > payment.get("expires_at", 0) else "pending", "amount_sats": payment["amount_sats"], "service": payment.get("service"), "created_at": payment.get("created_at"), "expires_at": payment.get("expires_at"), "paid_at": payment.get("paid_at"), "used": payment.get("used", False)}
325
+
326
+ @app.get("/api/v1/stats")
327
+ async def get_stats():
328
+ """Public statistics for AI discovery."""
329
+ total_requests = sum(len(usage) for usage in usage_db.values())
330
+ service_counts = defaultdict(int)
331
+ for usage_list in usage_db.values():
332
+ for usage in usage_list:
333
+ service_counts[usage["service"]] += 1
334
+ return {"total_requests": total_requests, "service_breakdown": dict(service_counts), "services": {"web_extraction": {"price_sats": 1000, "response_time_ms": 500, "rate_limit": "100/hour"}, "document_timestamp": {"price_sats": 5000, "response_time_ms": 200, "rate_limit": "50/hour"}, "api_aggregator": {"price_sats": 200, "response_time_ms": 300, "rate_limit": "1000/hour"}}, "uptime": "99.9%", "accepts_payment": ["bitcoin_lightning"], "version": "1.1.0", "features": ["idempotency", "rate_limiting", "audit_trail", "replay_protection", "receipts"]}
335
+
336
+ @app.get("/api/v1/audit")
337
+ async def get_audit_log(limit: int = 50):
338
+ """Recent audit log for agents to verify transaction history."""
339
+ return {"entries": audit_log[-limit:], "total_entries": len(audit_log)}
340
+
341
+ @app.get("/api/v1/rate-limit/{service}")
342
+ async def check_rate_limit_endpoint(service: str, req: Request):
343
+ """Check current rate limit status for your IP."""
344
+ client_ip = req.client.host
345
+ if service not in RATE_LIMITS:
346
+ raise HTTPException(status_code=404, detail="Unknown service")
347
+ return get_rate_limit_status(client_ip, service)
348
+
349
+ @app.get("/.well-known/ai-services.json")
350
+ async def ai_services_manifest():
351
+ """Machine-readable service description for AI agent discovery."""
352
+ return {"name": "UgarAPI", "description": "Automated services for AI agents", "version": "1.1.0", "payment_methods": ["bitcoin_lightning"], "features": {"idempotency": True, "rate_limiting": True, "audit_trail": True, "receipts": True, "replay_protection": True}, "error_codes": {"402": "Payment required - create invoice at /api/v1/payment/create", "429": "Rate limited - check /api/v1/rate-limit/{service}", "404": "Invoice not found", "500": "Service error - safe to retry with same idempotency_key"}, "retry_guidance": {"on_402": "Create new invoice and retry", "on_429": "Wait until reset_at timestamp", "on_500": "Retry with same idempotency_key - no double charge risk", "on_timeout": "Retry with same idempotency_key - no double charge risk"}, "services": [{"id": "web_extraction", "name": "Web Data Extraction", "description": "Extract structured data from any URL using CSS selectors", "endpoint": "/api/v1/extract", "price_sats": 1000, "price_usd_approx": 1.0, "rate_limit": "100/hour", "success_rate": 0.999, "supports_idempotency": True}, {"id": "document_timestamp", "name": "Document Timestamping", "description": "Cryptographically timestamp documents on Bitcoin blockchain", "endpoint": "/api/v1/timestamp", "price_sats": 5000, "price_usd_approx": 5.0, "rate_limit": "50/hour", "success_rate": 1.0, "supports_idempotency": True}, {"id": "api_aggregator", "name": "API Aggregator", "description": "Single endpoint for weather, maps, and data APIs", "endpoint": "/api/v1/aggregate", "price_sats": 200, "price_usd_approx": 0.20, "rate_limit": "1000/hour", "success_rate": 0.995, "supports_idempotency": True}], "contact": {"support": "support@ugarapi.com", "status_page": "https://ugarapi.com/health", "docs": "https://ugarapi.com/docs"}}
353
+
354
+ @app.get("/")
355
+ async def root():
356
+ return {"service": "UgarAPI", "version": "1.1.0", "status": "operational", "documentation": "/docs", "ai_manifest": "/.well-known/ai-services.json", "new_in_v1.1": ["Idempotency keys for safe retries", "Rate limiting per IP", "Full audit trail", "Replay attack protection", "Receipts on every transaction", "Payment status endpoint"]}
357
+
358
+ @app.get("/health")
359
+ async def health_check():
360
+ return {"status": "healthy", "version": "1.1.0", "timestamp": datetime.utcnow().isoformat()}
361
+
362
+ if __name__ == "__main__":
363
+ import uvicorn
364
+ uvicorn.run(app, host="0.0.0.0", port=8000)
365
+
366
+ @app.get("/badge")
367
+ async def product_hunt_badge():
368
+ from fastapi.responses import HTMLResponse
369
+ html = """<!DOCTYPE html>
370
+ <html>
371
+ <head>
372
+ <title>UgarAPI - Find us on Product Hunt</title>
373
+ <style>
374
+ body { font-family: monospace; background: #060608; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; gap: 30px; }
375
+ h1 { color: #f7931a; font-size: 2em; }
376
+ p { color: #999; max-width: 500px; text-align: center; line-height: 1.6; }
377
+ .links { display: flex; gap: 20px; }
378
+ a { color: #f7931a; text-decoration: none; }
379
+ a:hover { text-decoration: underline; }
380
+ </style>
381
+ </head>
382
+ <body>
383
+ <h1>⚡ UgarAPI</h1>
384
+ <p>Reliable API infrastructure for autonomous AI agents.<br>Pay per use via Bitcoin Lightning. No API keys. No subscriptions.</p>
385
+ <a href="https://www.producthunt.com/products/ugarapi?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-ugarapi" target="_blank" rel="noopener noreferrer">
386
+ <img alt="UgarAPI on Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1078085&theme=light&t=1770940864784">
387
+ </a>
388
+ <div class="links">
389
+ <a href="/docs">API Docs</a>
390
+ <a href="/.well-known/ai-services.json">AI Manifest</a>
391
+ <a href="/health">Health</a>
392
+ </div>
393
+ </body>
394
+ </html>"""
395
+ return HTMLResponse(content=html)
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ugarapi-mcp-server",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for UgarAPI - exposes AI services with Bitcoin Lightning payments",
5
+ "type": "module",
6
+ "main": "ugarapi-mcp-server.js",
7
+ "bin": {
8
+ "ugarapi-mcp": "./ugarapi-mcp-server.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node ugarapi-mcp-server.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "ai",
16
+ "agents",
17
+ "bitcoin",
18
+ "lightning",
19
+ "api"
20
+ ],
21
+ "author": "UgarAPI",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^0.5.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ }
29
+ }
@@ -0,0 +1,9 @@
1
+ fastapi==0.109.0
2
+ uvicorn[standard]==0.27.0
3
+ httpx==0.26.0
4
+ pydantic==2.5.3
5
+ beautifulsoup4==4.12.3
6
+ lxml==5.1.0
7
+ python-dotenv==1.0.0
8
+ psycopg2-binary==2.9.9
9
+ redis==5.0.1