-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathconfig.py
More file actions
186 lines (163 loc) · 6.24 KB
/
config.py
File metadata and controls
186 lines (163 loc) · 6.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
from __future__ import annotations
import logging
from functools import lru_cache
from urllib.parse import urlparse
from pydantic import Field, PositiveInt, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
logger = logging.getLogger(__name__)
class Settings(BaseSettings):
model_config = SettingsConfigDict(extra="ignore")
transport: str = Field(default="stdio")
everyrow_api_url: str = Field(default="https://everyrow.io/api/v0")
preview_size: int = Field(default=1000)
max_schema_properties: int = Field(
default=50,
description="Maximum number of properties allowed in a response schema",
)
token_budget: int = Field(
default=20000,
description="Target token budget per page of inline results",
)
redis_host: str = Field(default="localhost")
redis_port: int = Field(default=6379)
redis_db: int = Field(default=13)
redis_password: str | None = Field(default=None, repr=False)
redis_ssl: bool = Field(
default=False,
description="Enable TLS for Redis connections. Required when Redis is on a separate host.",
)
redis_sentinel_endpoints: str | None = Field(
default=None, description="Comma-separated host:port pairs"
)
redis_sentinel_master_name: str | None = Field(default=None)
trust_proxy_headers: bool = Field(
default=False,
description="Trust the header named by trusted_ip_header for client IP. "
"Enable only when behind a trusted reverse proxy.",
)
trusted_ip_header: str = Field(
default="X-Forwarded-For",
description="HTTP header containing the real client IP. "
"Use 'CF-Connecting-IP' behind Cloudflare, 'X-Forwarded-For' behind GKE/nginx.",
)
# HTTP-only settings — unused in stdio mode
mcp_server_url: str = Field(default="")
supabase_url: str = Field(default="")
supabase_anon_key: str = Field(default="", repr=False)
registration_rate_limit: PositiveInt = Field(
default=10,
description="Max registrations/authorizations per IP per rate window",
)
registration_rate_window: PositiveInt = Field(
default=60,
description="Rate limit sliding window in seconds",
)
access_token_ttl: PositiveInt = Field(
default=3300,
description="Access token TTL in seconds (55 min, before Supabase JWT 1h expiry)",
)
auth_code_ttl: PositiveInt = Field(
default=300,
description="Authorization code TTL in seconds",
)
pending_auth_ttl: PositiveInt = Field(
default=600,
description="Pending authorization TTL in seconds",
)
client_registration_ttl: PositiveInt = Field(
default=2_592_000,
description="Client registration TTL in seconds (30 days)",
)
refresh_token_ttl: PositiveInt = Field(
default=604_800,
description="Refresh token TTL in seconds (7 days)",
)
max_inline_rows: int = Field(
default=5000,
description="Maximum rows allowed in inline data (list[dict]).",
)
auto_page_size_threshold: int = Field(
default=50,
description="If total rows <= this value, skip asking the user for page_size and load all rows directly.",
)
# Upload settings (HTTP mode only)
upload_secret: str = Field(
default="",
description="HMAC-SHA256 secret for signing upload URLs. Required in HTTP mode.",
repr=False,
)
upload_url_ttl: int = Field(
default=300,
description="Presigned upload URL validity in seconds (5 min).",
)
max_upload_size_bytes: int = Field(
default=50 * 1024 * 1024,
description="Maximum upload file size in bytes (50 MB).",
)
max_upload_rows: int = Field(
default=50_000,
description="Maximum rows allowed in an uploaded CSV file.",
)
max_fetch_size_bytes: int = Field(
default=50 * 1024 * 1024,
description="Maximum response size when fetching CSV from a URL (50 MB).",
)
upload_rate_limit: PositiveInt = Field(
default=20,
description="Max uploads per user per rate window",
)
upload_rate_window: PositiveInt = Field(
default=3600,
description="Upload rate limit sliding window in seconds (1 hour)",
)
enable_sheets_tools: bool = Field(
default=False,
description="Enable Google Sheets tools (requires HTTP mode with Google OAuth)",
)
sheets_rate_limit: PositiveInt = Field(
default=60, description="Max sheets ops per user per rate window"
)
sheets_rate_window: PositiveInt = Field(
default=60, description="Sheets rate limit window in seconds"
)
everyrow_api_key: str | None = Field(default=None, repr=False)
@property
def is_http(self) -> bool:
return self.transport == "streamable-http"
@property
def is_stdio(self) -> bool:
return self.transport == "stdio"
@field_validator("mcp_server_url", "supabase_url")
@classmethod
def _validate_url(cls, v: str) -> str:
v = v.rstrip("/")
if not v:
return v
parsed = urlparse(v)
host = (parsed.hostname or "").lower()
is_local = host in ("localhost", "127.0.0.1", "::1")
if not is_local and parsed.scheme != "https":
raise ValueError(
f"Non-localhost URLs must use https:// (got {parsed.scheme}://)"
)
return v
@model_validator(mode="after")
def _require_redis_ssl_for_remote(self) -> Settings:
host = (self.redis_host or "").lower()
is_local = host in ("localhost", "127.0.0.1", "::1", "")
if not is_local and not self.redis_ssl:
if self.is_http:
raise ValueError(
f"Redis host {self.redis_host} is remote but redis_ssl=False. "
"Enable redis_ssl for non-localhost Redis in HTTP mode."
)
logger.warning(
"Redis host %s is remote but redis_ssl=False — traffic is unencrypted.",
self.redis_host,
)
return self
@lru_cache
def _get_settings():
settings_instance = Settings() # pyright: ignore[reportCallIssue]
return settings_instance
settings = _get_settings()