Skip to content

Commit cf9b64a

Browse files
committed
minor update on database connection
1 parent 2a6fd3b commit cf9b64a

4 files changed

Lines changed: 125 additions & 2 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ APP_LOG_LEVEL=INFO
1212
MCP_BASE_URL=http://localhost:8000/mcp
1313

1414
# Read-only PostgreSQL URL (asyncpg). Use a DB user with SELECT-only privileges in production.
15+
# `postgres://` is normalized to `postgresql+asyncpg://`, and `sslmode` is converted to `ssl`.
1516
# Example: postgresql+asyncpg://mcp_reader:CHANGE_ME@localhost:5432/cartesi_mcp
1617
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/mcp_server
1718

.github/workflows/fly.yaml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Deploy Release to Fly.io
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
concurrency:
8+
group: production-deploy
9+
cancel-in-progress: false
10+
11+
env:
12+
FLY_APP_NAME: cartesi-mcp-server
13+
FLY_ORG: mugen-builders
14+
IMAGE_NAME: ghcr.io/${{ github.repository }}
15+
16+
jobs:
17+
deploy-production:
18+
runs-on: ubuntu-latest
19+
20+
environment:
21+
name: production
22+
23+
permissions:
24+
contents: read
25+
packages: read
26+
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
31+
- name: Extract release version
32+
id: version
33+
run: |
34+
VERSION="${{ github.event.release.tag_name }}"
35+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
36+
37+
- name: Setup flyctl
38+
uses: superfly/flyctl-actions/setup-flyctl@master
39+
40+
- name: Sync production secrets to Fly.io
41+
env:
42+
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
43+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
44+
APP_NAME: ${{ secrets.APP_NAME }}
45+
APP_ENV: ${{ secrets.APP_ENV }}
46+
APP_HOST: ${{ secrets.APP_HOST }}
47+
APP_PORT: ${{ secrets.APP_PORT }}
48+
APP_LOG_LEVEL: ${{ secrets.APP_LOG_LEVEL }}
49+
MCP_BASE_URL: ${{ secrets.MCP_BASE_URL }}
50+
DEFAULT_PAGE_SIZE: ${{ secrets.DEFAULT_PAGE_SIZE }}
51+
MAX_PAGE_SIZE: ${{ secrets.MAX_PAGE_SIZE }}
52+
APP_ENV: production
53+
run: |
54+
flyctl secrets set \
55+
--app "$FLY_APP_NAME" \
56+
--stage \
57+
DATABASE_URL="$DATABASE_URL" \
58+
APP_NAME="$APP_NAME" \
59+
APP_ENV="$APP_ENV" \
60+
APP_HOST="$APP_HOST" \
61+
APP_PORT="$APP_PORT" \
62+
APP_LOG_LEVEL="$APP_LOG_LEVEL" \
63+
MCP_BASE_URL="$MCP_BASE_URL" \
64+
DEFAULT_PAGE_SIZE="$DEFAULT_PAGE_SIZE" \
65+
MAX_PAGE_SIZE="$MAX_PAGE_SIZE" \
66+
APP_ENV="$APP_ENV"
67+
68+
69+
- name: Deploy release image to existing Fly.io app
70+
env:
71+
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
72+
run: |
73+
flyctl deploy \
74+
--app "$FLY_APP_NAME" \
75+
--image "$IMAGE_NAME:${{ steps.version.outputs.version }}" \
76+
--strategy rolling \
77+
--wait-timeout 300

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Knowledge responses are **metadata and links** (titles, URIs, `canonical_url`, d
2121

2222
## Environment variables
2323

24-
Copy `.env.example` to `.env` and adjust. Defaults and field names are defined in `src/core/config.py` (notably `DATABASE_URL`, `APP_HOST`, `APP_PORT`, `MCP_BASE_URL`, pagination limits).
24+
Copy `.env.example` to `.env` and adjust. Defaults and field names are defined in `src/core/config.py` (notably `DATABASE_URL`, `APP_HOST`, `APP_PORT`, `MCP_BASE_URL`, pagination limits). For PostgreSQL URLs, `postgres://...` is normalized to `postgresql+asyncpg://...`, and `sslmode` is translated to asyncpg-compatible `ssl` (for example, `sslmode=disable` becomes `ssl=false`).
2525

2626
## Install
2727

src/core/config.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,47 @@
11
from functools import lru_cache
2+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
23

3-
from pydantic import Field
4+
from pydantic import Field, field_validator
45
from pydantic_settings import BaseSettings, SettingsConfigDict
56

67

8+
def normalize_database_url_for_async(url: str) -> str:
9+
u = url.strip()
10+
if u.startswith("postgres://"):
11+
u = "postgresql://" + u[len("postgres://") :]
12+
if u.startswith("postgresql://") and not u.startswith("postgresql+"):
13+
u = "postgresql+asyncpg://" + u[len("postgresql://") :]
14+
15+
# asyncpg does not accept sslmode=..., convert psycopg-style URLs.
16+
if u.startswith("postgresql+asyncpg://"):
17+
parsed = urlsplit(u)
18+
query = parse_qsl(parsed.query, keep_blank_values=True)
19+
has_ssl = any(key == "ssl" for key, _ in query)
20+
if not has_ssl:
21+
normalized_query: list[tuple[str, str]] = []
22+
for key, value in query:
23+
if key != "sslmode":
24+
normalized_query.append((key, value))
25+
continue
26+
27+
mode = value.strip().lower()
28+
if mode == "disable":
29+
normalized_query.append(("ssl", "false"))
30+
else:
31+
normalized_query.append(("ssl", "true"))
32+
33+
u = urlunsplit(
34+
(
35+
parsed.scheme,
36+
parsed.netloc,
37+
parsed.path,
38+
urlencode(normalized_query, doseq=True),
39+
parsed.fragment,
40+
)
41+
)
42+
return u
43+
44+
745
class Settings(BaseSettings):
846
app_name: str = Field(default="Cartesi Knowledge MCP Server", alias="APP_NAME")
947
app_env: str = Field(default="development", alias="APP_ENV")
@@ -20,6 +58,13 @@ class Settings(BaseSettings):
2058

2159
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
2260

61+
@field_validator("database_url", mode="before")
62+
@classmethod
63+
def _normalize_database_url(cls, v: object) -> object:
64+
if not isinstance(v, str):
65+
return v
66+
return normalize_database_url_for_async(v)
67+
2368

2469
@lru_cache(maxsize=1)
2570
def get_settings() -> Settings:

0 commit comments

Comments
 (0)