feat: add supabase local mcp#380
Conversation
- New supabase‑aidap package (config, models, platform, tools, utils) - pyproject.toml, uv.lock, README, .gitignore added - uvx script entry `supabase-aidap` for easy execution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
LGTM |
There was a problem hiding this comment.
Pull request overview
Adds a Supabase MCP server implementation backed by AIDAP, exposing workspace/branch, database, storage, and edge-function operations via FastMCP.
Changes:
- Introduces AIDAP and Supabase HTTP clients plus runtime/tool registration wiring for MCP.
- Adds tool implementations for workspaces/branches, SQL, storage buckets/config, and edge function CRUD/deploy.
- Adds shared utilities (JSON formatting, error decorators, target resolution), models, and documentation.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| server/mcp_server_supabase/src/mcp_server_supabase/utils/targets.py | Adds helpers to pick/resolve workspace vs branch targets. |
| server/mcp_server_supabase/src/mcp_server_supabase/utils/decorators.py | Adds async decorators for JSON serialization and error/read-only handling. |
| server/mcp_server_supabase/src/mcp_server_supabase/utils/common.py | Adds JSON helpers and generic field picking/compaction utilities. |
| server/mcp_server_supabase/src/mcp_server_supabase/utils/init.py | Exposes utils as a package API surface. |
| server/mcp_server_supabase/src/mcp_server_supabase/tools/workspace_tools.py | Implements workspace/branch listing, creation, deletion, endpoints, keys. |
| server/mcp_server_supabase/src/mcp_server_supabase/tools/storage_tools.py | Implements storage bucket/config operations via Supabase REST API. |
| server/mcp_server_supabase/src/mcp_server_supabase/tools/edge_function_tools.py | Implements edge function list/get/deploy/delete via Supabase REST API. |
| server/mcp_server_supabase/src/mcp_server_supabase/tools/database_tools.py | Implements SQL execution and schema/type generation utilities. |
| server/mcp_server_supabase/src/mcp_server_supabase/tools/base.py | Adds shared base class for resolving targets and building Supabase clients. |
| server/mcp_server_supabase/src/mcp_server_supabase/tools/init.py | Exposes tool classes as a package API surface. |
| server/mcp_server_supabase/src/mcp_server_supabase/tool_registry.py | Registers tool functions with FastMCP. |
| server/mcp_server_supabase/src/mcp_server_supabase/server.py | Adds server entrypoint and FastMCP initialization. |
| server/mcp_server_supabase/src/mcp_server_supabase/runtime.py | Adds runtime container and factory wiring clients/tools together. |
| server/mcp_server_supabase/src/mcp_server_supabase/platform/supabase_client.py | Adds async HTTP client wrapper with retries and structured errors. |
| server/mcp_server_supabase/src/mcp_server_supabase/platform/aidap_client.py | Adds AIDAP SDK wrapper for workspace/branch, endpoints, and key retrieval. |
| server/mcp_server_supabase/src/mcp_server_supabase/platform/init.py | Exposes platform clients as a package API surface. |
| server/mcp_server_supabase/src/mcp_server_supabase/models/workspace.py | Adds Pydantic models for workspace/branch/key structures. |
| server/mcp_server_supabase/src/mcp_server_supabase/models/storage.py | Adds Pydantic models for storage bucket/config structures. |
| server/mcp_server_supabase/src/mcp_server_supabase/models/edge_function.py | Adds Pydantic models for edge function structures. |
| server/mcp_server_supabase/src/mcp_server_supabase/models/database.py | Adds Pydantic models for database schema/migration structures. |
| server/mcp_server_supabase/src/mcp_server_supabase/models/init.py | Exposes models as a package API surface. |
| server/mcp_server_supabase/src/mcp_server_supabase/config.py | Adds env/config parsing and in-memory caches for endpoints/keys/branches. |
| server/mcp_server_supabase/src/mcp_server_supabase/init.py | Adds package metadata. |
| server/mcp_server_supabase/pyproject.toml | Defines packaging metadata, dependencies, and tooling config. |
| server/mcp_server_supabase/README_zh.md | Adds Chinese documentation for tools, config, and deployment. |
| server/mcp_server_supabase/README.md | Adds English documentation for tools, config, and deployment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if content: | ||
| response = await client.request( | ||
| method, url, content=content, headers=default_headers, | ||
| params=params, timeout=timeout | ||
| ) | ||
| else: | ||
| response = await client.request( | ||
| method, url, json=json_data, headers=default_headers, | ||
| params=params, timeout=timeout | ||
| ) |
There was a problem hiding this comment.
content is checked using truthiness, so content=b\"\" (or other falsy payloads) will incorrectly fall back to JSON mode. Use an explicit content is not None check so callers can intentionally send empty bodies/binary payloads.
| raise ValueError("Bucket name cannot be empty") | ||
| ws_id, branch_id = await self._resolve_target(workspace_id) | ||
| client = await self._get_client(ws_id, branch_id) | ||
| response = await client.call_api(f"/storage/v1/bucket/{bucket_name}", method="DELETE") |
There was a problem hiding this comment.
bucket_name is interpolated directly into the URL path without encoding, which can break requests for names containing reserved characters (e.g., spaces, /, ?) and may enable path confusion. URL-encode the path segment (e.g., with urllib.parse.quote) and use the stripped/normalized name consistently.
| "name": bucket_name, | ||
| "public": public | ||
| } | ||
| if file_size_limit: |
There was a problem hiding this comment.
This condition drops valid values like 0 because it relies on truthiness. If 0 is intended to be a meaningful limit value, check file_size_limit is not None instead.
| if file_size_limit: | |
| if file_size_limit is not None: |
| def handle_errors(func: Callable) -> Callable: | ||
| @wraps(func) | ||
| async def wrapper(*args, **kwargs) -> str: | ||
| try: | ||
| result = await func(*args, **kwargs) | ||
| if isinstance(result, str): | ||
| return result | ||
| if isinstance(result, list): | ||
| if result and hasattr(result[0], 'model_dump'): | ||
| result = [item.model_dump() for item in result] | ||
| elif hasattr(result, 'model_dump'): | ||
| result = result.model_dump() | ||
| return to_json(result) | ||
| except Exception as e: | ||
| error_msg = format_error(e) | ||
| logger.error(f"Error in {func.__name__}: {error_msg}") | ||
| return to_json({"error": error_msg}) | ||
| return wrapper |
There was a problem hiding this comment.
handle_errors coerces successful results into a JSON str (and also returns a JSON str on errors). Several decorated tool methods are annotated as returning dict/List[...]/Pydantic models, which no longer matches runtime behavior and can mislead FastMCP schema generation and static tooling. Either (a) update decorated method return annotations to str, or (b) change the decorator to return the original type and perform JSON serialization at the MCP boundary.
| logger.info(f"[DEBUG] Got endpoint for {workspace_id} branch={branch_id}: {endpoint}") | ||
| if not endpoint: | ||
| target = branch_id or workspace_id | ||
| raise ValueError(f"Could not get endpoint for target {target}") | ||
|
|
||
| api_key = await self.aidap.get_api_key(workspace_id, "service_role", branch_id=branch_id) | ||
| logger.info(f"[DEBUG] Got API key for {workspace_id} branch={branch_id}: {'yes' if api_key else 'no'}") |
There was a problem hiding this comment.
These are labeled as debug logs but logged at INFO, which will be noisy in production and may reveal internal endpoints. Consider switching to logger.debug(...) (and avoid printing the full endpoint), or gate behind a configurable debug flag.
| logger.info(f"[DEBUG] Got endpoint for {workspace_id} branch={branch_id}: {endpoint}") | |
| if not endpoint: | |
| target = branch_id or workspace_id | |
| raise ValueError(f"Could not get endpoint for target {target}") | |
| api_key = await self.aidap.get_api_key(workspace_id, "service_role", branch_id=branch_id) | |
| logger.info(f"[DEBUG] Got API key for {workspace_id} branch={branch_id}: {'yes' if api_key else 'no'}") | |
| logger.debug(f"[DEBUG] Got endpoint for {workspace_id} branch={branch_id}") | |
| if not endpoint: | |
| target = branch_id or workspace_id | |
| raise ValueError(f"Could not get endpoint for target {target}") | |
| api_key = await self.aidap.get_api_key(workspace_id, "service_role", branch_id=branch_id) | |
| logger.debug(f"[DEBUG] Got API key for {workspace_id} branch={branch_id}: {'yes' if api_key else 'no'}") |
| logging.basicConfig( | ||
| level=logging.INFO, | ||
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" | ||
| ) |
There was a problem hiding this comment.
Calling logging.basicConfig(...) and creating mcp = create_mcp() at import time introduces global side effects (affects host logging configuration; eagerly constructs runtime/client dependencies during import). Prefer moving basicConfig and MCP initialization into main() (or under the if __name__ == \"__main__\" guard), or provide a get_app() factory without module-level initialization.
| return mcp | ||
|
|
||
|
|
||
| mcp = create_mcp() |
There was a problem hiding this comment.
Calling logging.basicConfig(...) and creating mcp = create_mcp() at import time introduces global side effects (affects host logging configuration; eagerly constructs runtime/client dependencies during import). Prefer moving basicConfig and MCP initialization into main() (or under the if __name__ == \"__main__\" guard), or provide a get_app() factory without module-level initialization.
|
|
||
| ## License | ||
|
|
||
| volcengine/mcp-server is licensed under the [MIT License](https://github.com/volcengine/mcp-server/blob/main/LICENSE). |
There was a problem hiding this comment.
The README states the project is licensed under MIT, but pyproject.toml declares Apache-2.0. Please align the README and packaging metadata to reflect the actual license for this subproject.
| volcengine/mcp-server is licensed under the [MIT License](https://github.com/volcengine/mcp-server/blob/main/LICENSE). | |
| volcengine/mcp-server is licensed under the [Apache License 2.0](https://github.com/volcengine/mcp-server/blob/main/LICENSE). |
| edge_tools = runtime.edge_tools | ||
|
|
||
| @mcp.tool() | ||
| async def list_edge_functions(workspace_id: str = None) -> str: |
There was a problem hiding this comment.
These parameters are typed as str but default to None, which is inconsistent with type hints and can degrade generated schemas. Prefer Optional[str] (or str | None) for parameters that accept None.
| return await edge_tools.list_edge_functions(workspace_id) | ||
|
|
||
| @mcp.tool() | ||
| async def get_edge_function(function_name: str, workspace_id: str = None) -> str: |
There was a problem hiding this comment.
These parameters are typed as str but default to None, which is inconsistent with type hints and can degrade generated schemas. Prefer Optional[str] (or str | None) for parameters that accept None.
No description provided.