

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# CLAUDE.md - PostgreSQL MCP Server 开发指南23## 项目概述45PostgreSQL MCP Server 是一个基于 Model Context Protocol 的智能数据库查询服务,允许用户通过自然语言与 PostgreSQL 数据库交互。详细需求见 `specs/w5/0001-pg-mcp-prd.md`。67## 技术栈89- **Python**: 3.12+10- **MCP SDK**: FastMCP11- **PostgreSQL Driver**: asyncpg (异步) 或 psycopg312- **SQL Parser**: pglast (PostgreSQL 专用解析器)13- **LLM**: OpenAI SDK (gpt-5.2-mini)14- **配置管理**: pydantic-settings15- **测试**: pytest + pytest-asyncio + pytest-cov1617## 核心开发原则1819### Python Best Practices2021```python22# 1. 使用类型注解 (Type Hints)23from typing import Protocol, TypeVar, Generic24from collections.abc import Sequence, Mapping2526def query_database(sql: str, params: Mapping[str, Any] | None = None) -> list[dict[str, Any]]:27 ...2829# 2. 使用 dataclasses 或 Pydantic 定义数据模型30from pydantic import BaseModel, Field3132class QueryRequest(BaseModel):33 question: str = Field(..., min_length=1, max_length=10000)34 database: str | None = None35 return_type: Literal["sql", "result"] = "result"3637# 3. 使用 Enum 而非魔法字符串38from enum import StrEnum, auto3940class ErrorCode(StrEnum):41 SUCCESS = auto()42 SECURITY_VIOLATION = auto()43 SQL_PARSE_ERROR = auto()4445# 4. 使用 contextlib 管理资源46from contextlib import asynccontextmanager4748@asynccontextmanager49async def get_db_connection():50 conn = await asyncpg.connect(...)51 try:52 yield conn53 finally:54 await conn.close()5556# 5. 使用 __slots__ 优化内存 (高频对象)57class SchemaColumn:58 __slots__ = ("name", "type", "nullable", "default", "comment")59 ...60```6162### SOLID 原则应用6364#### Single Responsibility (单一职责)6566```67src/68├── core/69│ ├── schema_cache.py # 仅负责 Schema 缓存管理70│ ├── sql_generator.py # 仅负责 SQL 生成 (LLM 调用)71│ ├── sql_validator.py # 仅负责 SQL 安全验证72│ ├── sql_executor.py # 仅负责 SQL 执行73│ └── result_validator.py # 仅负责结果验证74├── models/75│ ├── schema.py # Schema 相关数据模型76│ ├── query.py # 查询请求/响应模型77│ └── errors.py # 错误定义78├── services/79│ └── query_service.py # 编排各个组件的服务层80└── config/81 └── settings.py # 配置管理82```8384#### Open/Closed (开闭原则)8586```python87# 使用 Protocol 定义接口,便于扩展88from typing import Protocol8990class SQLGenerator(Protocol):91 async def generate(self, question: str, schema: DatabaseSchema) -> str:92 """Generate SQL from natural language."""93 ...9495class OpenAISQLGenerator:96 """OpenAI 实现"""97 async def generate(self, question: str, schema: DatabaseSchema) -> str:98 ...99100class AnthropicSQLGenerator:101 """未来可扩展: Anthropic 实现"""102 async def generate(self, question: str, schema: DatabaseSchema) -> str:103 ...104```105106#### Liskov Substitution (里氏替换)107108```python109# 子类必须完全兼容父类接口110class BaseValidator(ABC):111 @abstractmethod112 def validate(self, sql: str) -> ValidationResult:113 ...114115class ReadOnlyValidator(BaseValidator):116 def validate(self, sql: str) -> ValidationResult:117 # 返回类型和行为与父类一致118 ...119```120121#### Interface Segregation (接口隔离)122123```python124# 细粒度接口,客户端只依赖需要的方法125class Readable(Protocol):126 async def read(self, key: str) -> Any: ...127128class Writable(Protocol):129 async def write(self, key: str, value: Any) -> None: ...130131class SchemaCache(Readable): # 只读缓存只实现 Readable132 async def read(self, key: str) -> Any: ...133```134135#### Dependency Inversion (依赖反转)136137```python138# 高层模块依赖抽象,而非具体实现139class QueryService:140 def __init__(141 self,142 generator: SQLGenerator, # 依赖抽象143 validator: SQLValidator, # 依赖抽象144 executor: SQLExecutor, # 依赖抽象145 ):146 self._generator = generator147 self._validator = validator148 self._executor = executor149```150151### DRY 原则152153```python154# 提取公共逻辑到工具函数155# utils/sql.py156def sanitize_identifier(name: str) -> str:157 """安全处理 SQL 标识符"""158 ...159160# 使用装饰器消除重复的横切关注点161from functools import wraps162163def with_timeout(seconds: float):164 def decorator(func):165 @wraps(func)166 async def wrapper(*args, **kwargs):167 async with asyncio.timeout(seconds):168 return await func(*args, **kwargs)169 return wrapper170 return decorator171172# 使用泛型减少重复代码173T = TypeVar("T")174175class Result(Generic[T]):176 def __init__(self, value: T | None, error: ErrorCode | None):177 self.value = value178 self.error = error179```180181## 代码质量要求182183### 必须遵循1841851. **类型完整**: 所有公开 API 必须有完整类型注解1862. **文档字符串**: 公开类和函数必须有 docstring (Google style)1873. **错误处理**: 使用自定义异常,不要裸露 `except`1884. **日志脱敏**: 绝不在日志中记录密钥、密码、PII 数据1895. **资源管理**: 使用 context manager 管理连接、文件等资源190191### 代码风格192193```bash194# 使用 ruff 进行 lint 和格式化195ruff check --fix .196ruff format .197198# pyproject.toml 配置199[tool.ruff]200target-version = "py312"201line-length = 100202203[tool.ruff.lint]204select = [205 "E", "F", "W", # pyflakes, pycodestyle206 "I", # isort207 "B", "C4", # bugbear, comprehensions208 "UP", # pyupgrade209 "SIM", # simplify210 "TCH", # type-checking imports211 "RUF", # ruff-specific212 "S", # security (bandit)213 "ASYNC", # async best practices214]215216[tool.ruff.lint.per-file-ignores]217"tests/**" = ["S101"] # allow assert in tests218```219220### 安全编码221222```python223# 1. 永远不要拼接 SQL224# BAD225sql = f"SELECT * FROM {table_name}"226227# GOOD - 使用参数化查询228sql = "SELECT * FROM $1"229await conn.fetch(sql, table_name)230231# 2. 验证所有外部输入232class QueryRequest(BaseModel):233 question: str = Field(..., min_length=1, max_length=10000)234235 @field_validator("question")236 @classmethod237 def sanitize_question(cls, v: str) -> str:238 # 移除潜在的 prompt injection239 return sanitize_user_input(v)240241# 3. 使用 pglast 解析和验证 SQL242from pglast import parse_sql, Node243244def validate_sql(sql: str) -> bool:245 try:246 stmts = parse_sql(sql)247 for stmt in stmts:248 if not isinstance(stmt.stmt, SelectStmt):249 raise SecurityViolationError("Only SELECT allowed")250 return True251 except ParseError:252 raise SQLParseError("Invalid SQL syntax")253```254255## 测试要求256257### 测试结构258259```260tests/261├── conftest.py # 共享 fixtures262├── unit/263│ ├── test_sql_validator.py264│ ├── test_sql_generator.py265│ └── test_schema_cache.py266├── integration/267│ ├── test_query_flow.py268│ └── test_db_connection.py269└── security/270 ├── test_sql_injection.py271 └── test_blocked_operations.py272```273274### 测试覆盖率要求275276- **总体覆盖率**: >= 80%277- **核心安全模块**: >= 95% (sql_validator, security checks)278- **分支覆盖**: 必须覆盖所有安全相关分支279280### 测试示例281282```python283# tests/unit/test_sql_validator.py284import pytest285from src.core.sql_validator import SQLValidator, SecurityViolationError286287class TestSQLValidator:288 @pytest.fixture289 def validator(self) -> SQLValidator:290 return SQLValidator(blocked_functions=["pg_sleep"])291292 @pytest.mark.parametrize("sql", [293 "SELECT * FROM users",294 "SELECT COUNT(*) FROM orders WHERE date > '2024-01-01'",295 "WITH cte AS (SELECT 1) SELECT * FROM cte",296 ])297 def test_valid_select_queries(self, validator: SQLValidator, sql: str):298 assert validator.validate(sql).is_valid299300 @pytest.mark.parametrize("sql,expected_error", [301 ("DELETE FROM users", "DELETE statement not allowed"),302 ("DROP TABLE users", "DROP statement not allowed"),303 ("SELECT pg_sleep(100)", "Function pg_sleep is blocked"),304 ("INSERT INTO logs VALUES (1)", "INSERT statement not allowed"),305 ])306 def test_blocked_operations(307 self, validator: SQLValidator, sql: str, expected_error: str308 ):309 with pytest.raises(SecurityViolationError, match=expected_error):310 validator.validate(sql)311312 def test_sql_injection_attempts(self, validator: SQLValidator):313 # 测试各种 SQL 注入变体314 injection_attempts = [315 "SELECT * FROM users; DROP TABLE users;--",316 "SELECT * FROM users WHERE id = 1 OR 1=1",317 "SELECT * FROM users UNION SELECT * FROM passwords",318 ]319 for sql in injection_attempts:320 result = validator.validate(sql)321 assert not result.allows_data_modification322323# tests/integration/test_query_flow.py324@pytest.mark.asyncio325async def test_end_to_end_query(326 query_service: QueryService,327 mock_openai: MockOpenAI,328 test_db: AsyncConnection,329):330 # Arrange331 mock_openai.set_response("SELECT COUNT(*) FROM users")332333 # Act334 result = await query_service.query(335 question="How many users are there?",336 return_type="result",337 )338339 # Assert340 assert result.success341 assert result.data.row_count == 1342 assert result.confidence >= 70343```344345### 测试运行346347```bash348# 运行所有测试349pytest350351# 运行并生成覆盖率报告352pytest --cov=src --cov-report=html --cov-fail-under=80353354# 只运行安全测试355pytest tests/security/ -v356357# 运行集成测试 (需要 PostgreSQL)358pytest tests/integration/ --db-url="postgresql://test@localhost/test"359```360361## 性能要求362363### 异步优先364365```python366# 使用 asyncpg 进行异步数据库操作367import asyncpg368369async def create_pool() -> asyncpg.Pool:370 return await asyncpg.create_pool(371 dsn=settings.database_url,372 min_size=5,373 max_size=20,374 command_timeout=30,375 )376377# 并发执行独立操作378async def load_all_schemas(databases: list[str]) -> dict[str, Schema]:379 tasks = [load_schema(db) for db in databases]380 results = await asyncio.gather(*tasks)381 return dict(zip(databases, results))382```383384### 缓存策略385386```python387from functools import lru_cache388from cachetools import TTLCache389390# 内存缓存 Schema391class SchemaCache:392 def __init__(self, ttl_seconds: int = 3600):393 self._cache: TTLCache[str, DatabaseSchema] = TTLCache(394 maxsize=100, ttl=ttl_seconds395 )396397 async def get_schema(self, database: str) -> DatabaseSchema:398 if database not in self._cache:399 self._cache[database] = await self._load_schema(database)400 return self._cache[database]401```402403### 连接池管理404405```python406# 使用连接池避免频繁建立连接407class DatabaseManager:408 def __init__(self):409 self._pools: dict[str, asyncpg.Pool] = {}410411 async def get_connection(self, database: str) -> asyncpg.Connection:412 if database not in self._pools:413 self._pools[database] = await asyncpg.create_pool(...)414 return await self._pools[database].acquire()415```416417## 项目配置418419### pyproject.toml 完整配置420421```toml422[project]423name = "pg-mcp"424version = "0.1.0"425description = "PostgreSQL MCP Server for natural language queries"426requires-python = ">=3.12"427dependencies = [428 "fastmcp>=2.14.1",429 "asyncpg>=0.29.0",430 "pglast>=6.0",431 "openai>=1.0.0",432 "pydantic>=2.0",433 "pydantic-settings>=2.0",434 "structlog>=24.0",435]436437[project.optional-dependencies]438dev = [439 "pytest>=8.0",440 "pytest-asyncio>=0.23",441 "pytest-cov>=4.0",442 "ruff>=0.4",443 "mypy>=1.10",444 "pre-commit>=3.0",445]446447[tool.pytest.ini_options]448asyncio_mode = "auto"449testpaths = ["tests"]450addopts = "-v --tb=short"451452[tool.mypy]453python_version = "3.12"454strict = true455warn_return_any = true456warn_unused_ignores = true457458[tool.coverage.run]459source = ["src"]460branch = true461462[tool.coverage.report]463exclude_lines = [464 "pragma: no cover",465 "if TYPE_CHECKING:",466 "@abstractmethod",467]468```469470## 常用命令471472```bash473# 安装依赖474uv sync475476# 运行服务477uv run python main.py478479# 运行测试480uv run pytest481482# 类型检查483uv run mypy src484485# Lint 和格式化486uv run ruff check --fix .487uv run ruff format .488489# 生成覆盖率报告490uv run pytest --cov=src --cov-report=html491```492493## 错误处理模式494495```python496# 定义项目异常层次497class PgMcpError(Exception):498 """Base exception for pg-mcp"""499 def __init__(self, message: str, code: ErrorCode):500 super().__init__(message)501 self.code = code502503class SecurityViolationError(PgMcpError):504 def __init__(self, message: str):505 super().__init__(message, ErrorCode.SECURITY_VIOLATION)506507class SQLParseError(PgMcpError):508 def __init__(self, message: str):509 super().__init__(message, ErrorCode.SQL_PARSE_ERROR)510511# 统一错误处理512async def handle_query(request: QueryRequest) -> QueryResponse:513 try:514 return await _process_query(request)515 except SecurityViolationError as e:516 return QueryResponse(517 success=False,518 error=ErrorInfo(code=e.code, message=str(e)),519 )520 except PgMcpError as e:521 logger.warning("Query failed", error=str(e), code=e.code)522 return QueryResponse(success=False, error=ErrorInfo(code=e.code, message=str(e)))523 except Exception:524 logger.exception("Unexpected error")525 return QueryResponse(526 success=False,527 error=ErrorInfo(code=ErrorCode.INTERNAL_ERROR, message="Internal error"),528 )529```530531## Git 提交规范532533```534feat: 新功能535fix: Bug 修复536docs: 文档更新537refactor: 重构 (不改变功能)538test: 测试相关539perf: 性能优化540security: 安全相关修复541```542543## 检查清单544545在提交 PR 前确保:546547- [ ] 所有测试通过 (`pytest`)548- [ ] 类型检查通过 (`mypy src`)549- [ ] Lint 检查通过 (`ruff check .`)550- [ ] 安全测试覆盖新增代码路径551- [ ] 敏感信息未暴露在日志中552- [ ] 文档已更新 (如适用)553
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| tyrchen/geektime-bootcamp-ai.cursor/rules/python-fastapi-backend.mdc · 230 | Cursor rules | setuptestlint-formatstyle+7 | 84/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-ai.cursor/rules/rust-best-practices.mdc · 230 | Cursor rules | teststylearchdependencies+1 | 81/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-ai.cursor/rules/specify-rules.mdc · 230 | Cursor rules | stylearch | 52/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aisite/CLAUDE.md · 230 | CLAUDE.md | agent-behaviour | 25/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw3/raflow/CLAUDE.md · 230 | CLAUDE.md | agent-behaviour | 25/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/codereview-agent/CLAUDE.md · 230 | CLAUDE.md | setupbuildlint-formatarch+4 | 90/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/opencode-introspection/CLAUDE.md · 230 | CLAUDE.md | setupbuildtestlint-format+10 | 84/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/opencode-introspection/visualizer/CLAUDE.md · 230 | CLAUDE.md | setupbuildtestlint-format+9 | 78/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw6/simple-agent/CLAUDE.md · 230 | CLAUDE.md | setupbuildtestlint-format+10 | 84/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| tyrchen/geektime-bootcamp-aiw7/genslides/frontend/CLAUDE.md · 230 | CLAUDE.md | teststylearchtypes+5 | 88/100 | 9 days ago |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 7 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 46 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 14 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.5k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 14 days ago | |
| microsoft/playwrightCLAUDE.md · 95k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 7 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/tyrchen-geektime-bootcamp-ai-w5-pg-mcp-claude)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.