Skip to content

Commit 16309e6

Browse files
feat: setup API client <> API server integration
1 parent 03c62f6 commit 16309e6

File tree

14 files changed

+216
-145
lines changed

14 files changed

+216
-145
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ SUPABASE_REGION=us-east-1 # Region where your Supabase project is hosted
1414
SUPABASE_ACCESS_TOKEN=your-personal-access-token # Required for Management API tools
1515
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Required for Auth Admin SDK tools
1616

17+
18+
# ONLY for local development
19+
QUERY_API_URL=http://127.0.0.1:8080/v1 # TheQuery.dev API URL when developing locally

.env.test.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ SUPABASE_DB_PASSWORD=postgres
1010

1111
# Optional: Service role key (for auth tests)
1212
# SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
13+
14+
# TheQuery.dev API URL
15+
QUERY_API_URL=http://127.0.0.1:8080/v1

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ readme = "README.md"
1010
requires-python = ">=3.12"
1111
dependencies = [
1212
"asyncpg>=0.30.0",
13-
"mcp[cli]>=1.2.1",
13+
"mcp[cli]>=1.4.1",
1414
"pglast>=7.3",
1515
"pyyaml>=6.0.2",
1616
"supabase>=2.13.0",

supabase_mcp/clients/api_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(
4141
self.query_api_url = query_api_url or settings.query_api_url
4242
self._check_api_key_set()
4343
self.client: httpx.AsyncClient | None = None
44-
logger.info(f"Query API client initialized with key: {self.query_api_key}")
44+
logger.info(f"Query API client initialized with URL: {self.query_api_url}, key: {self.query_api_key}")
4545

4646
async def _ensure_client(self) -> httpx.AsyncClient:
4747
"""Ensure client exists and is ready for use.

supabase_mcp/core/container.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from mcp.server.fastmcp import FastMCP
1+
from __future__ import annotations
22

33
from supabase_mcp.clients.api_client import ApiClient
44
from supabase_mcp.clients.management_client import ManagementAPIClient
@@ -14,10 +14,14 @@
1414
from supabase_mcp.tools import ToolManager
1515

1616

17-
class Container:
17+
class ServicesContainer:
18+
"""Container for all services"""
19+
20+
_instance: ServicesContainer | None = None
21+
1822
def __init__(
1923
self,
20-
mcp_server: FastMCP,
24+
# mcp_server: FastMCP,
2125
postgres_client: PostgresClient | None = None,
2226
api_client: ManagementAPIClient | None = None,
2327
sdk_client: SupabaseSDKClient | None = None,
@@ -26,9 +30,11 @@ def __init__(
2630
query_manager: QueryManager | None = None,
2731
tool_manager: ToolManager | None = None,
2832
log_manager: LogManager | None = None,
33+
query_api_client: ApiClient | None = None,
34+
feature_manager: FeatureManager | None = None,
2935
) -> None:
3036
"""Create a new container container reference"""
31-
self.mcp_server = mcp_server
37+
# self.mcp_server = mcp_server
3238
self.postgres_client = postgres_client
3339
self.api_client = api_client
3440
self.api_manager = api_manager
@@ -37,8 +43,17 @@ def __init__(
3743
self.query_manager = query_manager
3844
self.tool_manager = tool_manager
3945
self.log_manager = log_manager
46+
self.query_api_client = query_api_client
47+
self.feature_manager = feature_manager
4048

41-
def initialize(self, settings: Settings) -> "Container":
49+
@classmethod
50+
def get_instance(cls) -> ServicesContainer:
51+
"""Get the singleton instance of the container"""
52+
if cls._instance is None:
53+
cls._instance = cls()
54+
return cls._instance
55+
56+
def initialize_services(self, settings: Settings) -> None:
4257
"""Initializes all services in a synchronous manner to satisfy MCP runtime requirements"""
4358
# Create clients
4459
self.postgres_client = PostgresClient.get_instance(settings=settings)
@@ -62,13 +77,11 @@ def initialize(self, settings: Settings) -> "Container":
6277

6378
# Create query api client
6479
self.query_api_client = ApiClient()
65-
self.feature_manager = FeatureManager(self.query_api_client, self)
80+
self.feature_manager = FeatureManager(self.query_api_client)
6681

6782
logger.info("✓ All services initialized successfully.")
6883

69-
return self
70-
71-
async def close(self) -> None:
84+
async def shutdown_services(self) -> None:
7285
"""Properly close all relevant clients and connections"""
7386
# Postgres client
7487
if self.postgres_client:

supabase_mcp/core/feature_manager.py

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
from typing import Any, Literal
1+
from typing import TYPE_CHECKING, Any, Literal
22

33
from supabase_mcp.clients.api_client import ApiClient
4-
from supabase_mcp.core.container import Container
54
from supabase_mcp.exceptions import APIError, ConfirmationRequiredError, FeatureAccessError, FeatureTemporaryError
65
from supabase_mcp.logger import logger
76
from supabase_mcp.services.database.postgres_client import QueryResult
87
from supabase_mcp.services.safety.models import ClientType, SafetyMode
98
from supabase_mcp.tools.manager import ToolName
109

10+
if TYPE_CHECKING:
11+
from supabase_mcp.core.container import ServicesContainer
12+
1113

1214
class FeatureManager:
1315
"""Service for managing features, access to them and their configuration."""
@@ -17,7 +19,6 @@ def __init__(self, api_client: ApiClient):
1719
1820
Args:
1921
api_client: Client for communicating with the API
20-
container: Services container for accessing other services
2122
"""
2223
self.api_client = api_client
2324

@@ -50,99 +51,104 @@ async def check_feature_access(self, feature_name: str) -> None:
5051
raise FeatureTemporaryError(feature_name) from e
5152
raise
5253

53-
# Tool implementations moved from registry.py
54-
55-
async def execute_tool(self, tool_name: str, service_container: Container, **kwargs: Any) -> Any:
54+
async def execute_tool(self, tool_name: ToolName, services_container: "ServicesContainer", **kwargs: Any) -> Any:
5655
"""Execute a tool with feature access check.
5756
5857
Args:
5958
tool_name: Name of the tool to execute
59+
services_container: Container with all services
6060
**kwargs: Arguments to pass to the tool
6161
6262
Returns:
6363
Result of the tool execution
6464
"""
6565
# Check feature access
66-
await self.check_feature_access(tool_name)
66+
await self.check_feature_access(tool_name.value)
6767

6868
# Execute the appropriate tool based on name
6969
if tool_name == ToolName.GET_SCHEMAS:
70-
return await self.get_schemas()
70+
return await self.get_schemas(services_container)
7171
elif tool_name == ToolName.GET_TABLES:
72-
return await self.get_tables(**kwargs)
72+
return await self.get_tables(services_container, **kwargs)
7373
elif tool_name == ToolName.GET_TABLE_SCHEMA:
74-
return await self.get_table_schema(**kwargs)
74+
return await self.get_table_schema(services_container, **kwargs)
7575
elif tool_name == ToolName.EXECUTE_POSTGRESQL:
76-
return await self.execute_postgresql(**kwargs)
76+
return await self.execute_postgresql(services_container, **kwargs)
7777
elif tool_name == ToolName.RETRIEVE_MIGRATIONS:
78-
return await self.retrieve_migrations(**kwargs)
78+
return await self.retrieve_migrations(services_container, **kwargs)
7979
elif tool_name == ToolName.SEND_MANAGEMENT_API_REQUEST:
80-
return await self.send_management_api_request(**kwargs)
80+
return await self.send_management_api_request(services_container, **kwargs)
8181
elif tool_name == ToolName.GET_MANAGEMENT_API_SPEC:
82-
return await self.get_management_api_spec(**kwargs)
82+
return await self.get_management_api_spec(services_container, **kwargs)
8383
elif tool_name == ToolName.GET_AUTH_ADMIN_METHODS_SPEC:
84-
return await self.get_auth_admin_methods_spec()
84+
return await self.get_auth_admin_methods_spec(services_container)
8585
elif tool_name == ToolName.CALL_AUTH_ADMIN_METHOD:
86-
return await self.call_auth_admin_method(**kwargs)
86+
return await self.call_auth_admin_method(services_container, **kwargs)
8787
elif tool_name == ToolName.LIVE_DANGEROUSLY:
88-
return await self.live_dangerously(**kwargs)
88+
return await self.live_dangerously(services_container, **kwargs)
8989
elif tool_name == ToolName.CONFIRM_DESTRUCTIVE_OPERATION:
90-
return await self.confirm_destructive_operation(**kwargs)
90+
return await self.confirm_destructive_operation(services_container, **kwargs)
9191
elif tool_name == ToolName.RETRIEVE_LOGS:
92-
return await self.retrieve_logs(**kwargs)
92+
return await self.retrieve_logs(services_container, **kwargs)
9393
else:
9494
raise ValueError(f"Unknown tool: {tool_name}")
9595

96-
async def get_schemas(self) -> QueryResult:
96+
async def get_schemas(self, container: "ServicesContainer") -> QueryResult:
9797
"""List all database schemas with their sizes and table counts."""
98-
query_manager = self.container.query_manager
98+
query_manager = container.query_manager
9999
query = query_manager.get_schemas_query()
100100
return await query_manager.handle_query(query)
101101

102-
async def get_tables(self, schema_name: str) -> QueryResult:
102+
async def get_tables(self, container: "ServicesContainer", schema_name: str) -> QueryResult:
103103
"""List all tables, foreign tables, and views in a schema with their sizes, row counts, and metadata."""
104-
query_manager = self.container.query_manager
104+
query_manager = container.query_manager
105105
query = query_manager.get_tables_query(schema_name)
106106
return await query_manager.handle_query(query)
107107

108-
async def get_table_schema(self, schema_name: str, table: str) -> QueryResult:
108+
async def get_table_schema(self, container: "ServicesContainer", schema_name: str, table: str) -> QueryResult:
109109
"""Get detailed table structure including columns, keys, and relationships."""
110-
query_manager = self.container.query_manager
110+
query_manager = container.query_manager
111111
query = query_manager.get_table_schema_query(schema_name, table)
112112
return await query_manager.handle_query(query)
113113

114-
async def execute_postgresql(self, query: str, migration_name: str = "") -> QueryResult:
114+
async def execute_postgresql(
115+
self, container: "ServicesContainer", query: str, migration_name: str = ""
116+
) -> QueryResult:
115117
"""Execute PostgreSQL statements against your Supabase database."""
116-
query_manager = self.container.query_manager
118+
query_manager = container.query_manager
117119
return await query_manager.handle_query(query, has_confirmation=False, migration_name=migration_name)
118120

119121
async def retrieve_migrations(
120122
self,
123+
container: "ServicesContainer",
121124
limit: int = 50,
122125
offset: int = 0,
123126
name_pattern: str = "",
124127
include_full_queries: bool = False,
125128
) -> QueryResult:
126129
"""Retrieve a list of all migrations a user has from Supabase."""
127-
query_manager = self.container.query_manager
130+
query_manager = container.query_manager
128131
query = query_manager.get_migrations_query(
129132
limit=limit, offset=offset, name_pattern=name_pattern, include_full_queries=include_full_queries
130133
)
131134
return await query_manager.handle_query(query)
132135

133136
async def send_management_api_request(
134137
self,
138+
container: "ServicesContainer",
135139
method: str,
136140
path: str,
137141
path_params: dict[str, str],
138142
request_params: dict[str, Any],
139143
request_body: dict[str, Any],
140144
) -> dict[str, Any]:
141145
"""Execute a Supabase Management API request."""
142-
api_manager = self.container.api_manager
146+
api_manager = container.api_manager
143147
return await api_manager.execute_request(method, path, path_params, request_params, request_body)
144148

145-
async def get_management_api_spec(self, params: dict[str, Any] = {}) -> dict[str, Any]:
149+
async def get_management_api_spec(
150+
self, container: "ServicesContainer", params: dict[str, Any] = {}
151+
) -> dict[str, Any]:
146152
"""Get the Supabase Management API specification."""
147153
path = params.get("path")
148154
method = params.get("method")
@@ -152,21 +158,23 @@ async def get_management_api_spec(self, params: dict[str, Any] = {}) -> dict[str
152158
logger.debug(
153159
f"Getting management API spec with path: {path}, method: {method}, domain: {domain}, all_paths: {all_paths}"
154160
)
155-
api_manager = self.container.api_manager
161+
api_manager = container.api_manager
156162
return await api_manager.handle_spec_request(path, method, domain, all_paths)
157163

158-
async def get_auth_admin_methods_spec(self) -> dict[str, Any]:
164+
async def get_auth_admin_methods_spec(self, container: "ServicesContainer") -> dict[str, Any]:
159165
"""Get Python SDK methods specification for Auth Admin."""
160-
sdk_client = self.container.sdk_client
166+
sdk_client = container.sdk_client
161167
return sdk_client.return_python_sdk_spec()
162168

163-
async def call_auth_admin_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
169+
async def call_auth_admin_method(
170+
self, container: "ServicesContainer", method: str, params: dict[str, Any]
171+
) -> dict[str, Any]:
164172
"""Call an Auth Admin method from Supabase Python SDK."""
165-
sdk_client = self.container.sdk_client
173+
sdk_client = container.sdk_client
166174
return await sdk_client.call_auth_admin_method(method, params)
167175

168176
async def live_dangerously(
169-
self, service: Literal["api", "database"], enable_unsafe_mode: bool = False
177+
self, container: "ServicesContainer", service: Literal["api", "database"], enable_unsafe_mode: bool = False
170178
) -> dict[str, Any]:
171179
"""
172180
Toggle between safe and unsafe operation modes for API or Database services.
@@ -175,7 +183,7 @@ async def live_dangerously(
175183
- Enable write operations for the database (INSERT, UPDATE, DELETE, schema changes)
176184
- Enable state-changing operations for the Management API
177185
"""
178-
safety_manager = self.container.safety_manager
186+
safety_manager = container.safety_manager
179187
if service == "api":
180188
# Set the safety mode in the safety manager
181189
new_mode = SafetyMode.UNSAFE if enable_unsafe_mode else SafetyMode.SAFE
@@ -192,11 +200,15 @@ async def live_dangerously(
192200
return {"service": "database", "mode": safety_manager.get_safety_mode(ClientType.DATABASE)}
193201

194202
async def confirm_destructive_operation(
195-
self, operation_type: Literal["api", "database"], confirmation_id: str, user_confirmation: bool = False
203+
self,
204+
container: "ServicesContainer",
205+
operation_type: Literal["api", "database"],
206+
confirmation_id: str,
207+
user_confirmation: bool = False,
196208
) -> QueryResult | dict[str, Any]:
197209
"""Execute a destructive operation after confirmation. Use this only after reviewing the risks with the user."""
198-
api_manager = self.container.api_manager
199-
query_manager = self.container.query_manager
210+
api_manager = container.api_manager
211+
query_manager = container.query_manager
200212
if not user_confirmation:
201213
raise ConfirmationRequiredError("Destructive operation requires explicit user confirmation.")
202214

@@ -207,6 +219,7 @@ async def confirm_destructive_operation(
207219

208220
async def retrieve_logs(
209221
self,
222+
container: "ServicesContainer",
210223
collection: str,
211224
limit: int = 20,
212225
hours_ago: int = 1,
@@ -219,7 +232,7 @@ async def retrieve_logs(
219232
f"Tool called: retrieve_logs(collection={collection}, limit={limit}, hours_ago={hours_ago}, filters={filters}, search={search}, custom_query={'<custom>' if custom_query else None})"
220233
)
221234

222-
api_manager = self.container.api_manager
235+
api_manager = container.api_manager
223236
result = await api_manager.retrieve_logs(
224237
collection=collection,
225238
limit=limit,

supabase_mcp/main.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1+
from contextlib import asynccontextmanager
2+
13
from mcp.server.fastmcp import FastMCP
24

3-
from supabase_mcp.core.container import Container
5+
from supabase_mcp.core.container import ServicesContainer
46
from supabase_mcp.logger import logger
57
from supabase_mcp.settings import settings
68
from supabase_mcp.tools.registry import ToolRegistry
79

8-
# Create mcp instance
9-
mcp = FastMCP("supabase")
1010

11-
# Initialize services
12-
services_container = Container(mcp_server=mcp).initialize(settings)
11+
# Create lifespan for the MCP server
12+
@asynccontextmanager
13+
async def lifespan(app: FastMCP):
14+
try:
15+
# Initialize services
16+
services_container = ServicesContainer.get_instance()
17+
services_container.initialize_services(settings)
18+
19+
# Register tools
20+
mcp = ToolRegistry(mcp=app, services_container=services_container).register_tools()
21+
yield mcp
22+
finally:
23+
services_container = ServicesContainer.get_instance()
24+
await services_container.shutdown_services()
1325

14-
# Register tools
15-
mcp = ToolRegistry(mcp=mcp, services_container=services_container).register_tools()
26+
27+
# Create mcp instance
28+
mcp = FastMCP("supabase", lifespan=lifespan)
1629

1730

1831
def run_server() -> None:

supabase_mcp/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class Settings(BaseSettings):
101101
query_api_url: str = Field(
102102
default="https://api.thequery.dev/v1",
103103
description="TheQuery.dev API URL",
104+
alias="QUERY_API_URL",
104105
)
105106

106107
@field_validator("supabase_region")

0 commit comments

Comments
 (0)