thrivekit 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/explain.md +114 -0
- package/.claude/commands/idea.md +370 -0
- package/.claude/commands/my-dna.md +122 -0
- package/.claude/commands/prd.md +286 -0
- package/.claude/commands/review.md +167 -0
- package/.claude/commands/sign.md +32 -0
- package/.claude/commands/styleguide.md +450 -0
- package/.claude/commands/tour.md +301 -0
- package/.claude/commands/vibe-check.md +116 -0
- package/.claude/commands/vibe-help.md +47 -0
- package/.claude/commands/vibe-list.md +203 -0
- package/.claude/settings.json +75 -0
- package/.claude/settings.local.json +12 -0
- package/.pre-commit-hooks.yaml +102 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/postinstall.sh +29 -0
- package/bin/ralph.sh +171 -0
- package/bin/thrivekit.sh +24 -0
- package/bin/vibe-check.js +19 -0
- package/dist/checks/check-any-types.d.ts +6 -0
- package/dist/checks/check-any-types.d.ts.map +1 -0
- package/dist/checks/check-any-types.js +73 -0
- package/dist/checks/check-any-types.js.map +1 -0
- package/dist/checks/check-commented-code.d.ts +6 -0
- package/dist/checks/check-commented-code.d.ts.map +1 -0
- package/dist/checks/check-commented-code.js +81 -0
- package/dist/checks/check-commented-code.js.map +1 -0
- package/dist/checks/check-console-error.d.ts +6 -0
- package/dist/checks/check-console-error.d.ts.map +1 -0
- package/dist/checks/check-console-error.js +41 -0
- package/dist/checks/check-console-error.js.map +1 -0
- package/dist/checks/check-debug-statements.d.ts +6 -0
- package/dist/checks/check-debug-statements.d.ts.map +1 -0
- package/dist/checks/check-debug-statements.js +120 -0
- package/dist/checks/check-debug-statements.js.map +1 -0
- package/dist/checks/check-deep-nesting.d.ts +6 -0
- package/dist/checks/check-deep-nesting.d.ts.map +1 -0
- package/dist/checks/check-deep-nesting.js +116 -0
- package/dist/checks/check-deep-nesting.js.map +1 -0
- package/dist/checks/check-docker-platform.d.ts +6 -0
- package/dist/checks/check-docker-platform.d.ts.map +1 -0
- package/dist/checks/check-docker-platform.js +42 -0
- package/dist/checks/check-docker-platform.js.map +1 -0
- package/dist/checks/check-dry-violations.d.ts +6 -0
- package/dist/checks/check-dry-violations.d.ts.map +1 -0
- package/dist/checks/check-dry-violations.js +124 -0
- package/dist/checks/check-dry-violations.js.map +1 -0
- package/dist/checks/check-empty-catch.d.ts +6 -0
- package/dist/checks/check-empty-catch.d.ts.map +1 -0
- package/dist/checks/check-empty-catch.js +111 -0
- package/dist/checks/check-empty-catch.js.map +1 -0
- package/dist/checks/check-function-length.d.ts +6 -0
- package/dist/checks/check-function-length.d.ts.map +1 -0
- package/dist/checks/check-function-length.js +152 -0
- package/dist/checks/check-function-length.js.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts +10 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.js +102 -0
- package/dist/checks/check-hardcoded-ai-models.js.map +1 -0
- package/dist/checks/check-hardcoded-urls.d.ts +6 -0
- package/dist/checks/check-hardcoded-urls.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-urls.js +124 -0
- package/dist/checks/check-hardcoded-urls.js.map +1 -0
- package/dist/checks/check-magic-numbers.d.ts +6 -0
- package/dist/checks/check-magic-numbers.d.ts.map +1 -0
- package/dist/checks/check-magic-numbers.js +116 -0
- package/dist/checks/check-magic-numbers.js.map +1 -0
- package/dist/checks/check-secrets.d.ts +6 -0
- package/dist/checks/check-secrets.d.ts.map +1 -0
- package/dist/checks/check-secrets.js +138 -0
- package/dist/checks/check-secrets.js.map +1 -0
- package/dist/checks/check-snake-case-ts.d.ts +6 -0
- package/dist/checks/check-snake-case-ts.d.ts.map +1 -0
- package/dist/checks/check-snake-case-ts.js +78 -0
- package/dist/checks/check-snake-case-ts.js.map +1 -0
- package/dist/checks/check-todo-fixme.d.ts +6 -0
- package/dist/checks/check-todo-fixme.d.ts.map +1 -0
- package/dist/checks/check-todo-fixme.js +41 -0
- package/dist/checks/check-todo-fixme.js.map +1 -0
- package/dist/checks/check-unsafe-html.d.ts +6 -0
- package/dist/checks/check-unsafe-html.d.ts.map +1 -0
- package/dist/checks/check-unsafe-html.js +101 -0
- package/dist/checks/check-unsafe-html.js.map +1 -0
- package/dist/checks/index.d.ts +30 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +57 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/file-reader.d.ts +24 -0
- package/dist/utils/file-reader.d.ts.map +1 -0
- package/dist/utils/file-reader.js +140 -0
- package/dist/utils/file-reader.js.map +1 -0
- package/dist/utils/patterns.d.ts +27 -0
- package/dist/utils/patterns.d.ts.map +1 -0
- package/dist/utils/patterns.js +84 -0
- package/dist/utils/patterns.js.map +1 -0
- package/dist/utils/reporters.d.ts +21 -0
- package/dist/utils/reporters.d.ts.map +1 -0
- package/dist/utils/reporters.js +115 -0
- package/dist/utils/reporters.js.map +1 -0
- package/dist/utils/types.d.ts +71 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +5 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +82 -0
- package/ralph/api.sh +210 -0
- package/ralph/backup.sh +838 -0
- package/ralph/browser-verify/README.md +135 -0
- package/ralph/browser-verify/verify.ts +450 -0
- package/ralph/checks/check-fastapi-responses.py +155 -0
- package/ralph/hooks/hooks-config.json +72 -0
- package/ralph/hooks/inject-context.sh +44 -0
- package/ralph/hooks/install.sh +207 -0
- package/ralph/hooks/log-tools.sh +45 -0
- package/ralph/hooks/protect-prd.sh +27 -0
- package/ralph/hooks/save-learnings.sh +36 -0
- package/ralph/hooks/warn-debug.sh +54 -0
- package/ralph/hooks/warn-empty-catch.sh +63 -0
- package/ralph/hooks/warn-secrets.sh +89 -0
- package/ralph/hooks/warn-urls.sh +77 -0
- package/ralph/init.sh +388 -0
- package/ralph/loop.sh +570 -0
- package/ralph/playwright.sh +238 -0
- package/ralph/prd.sh +295 -0
- package/ralph/setup/feature-tour.sh +155 -0
- package/ralph/setup/quick-setup.sh +239 -0
- package/ralph/setup/tutorial.sh +159 -0
- package/ralph/setup/ui.sh +136 -0
- package/ralph/setup.sh +353 -0
- package/ralph/signs.sh +150 -0
- package/ralph/utils.sh +682 -0
- package/ralph/verify/browser.sh +324 -0
- package/ralph/verify/lint.sh +363 -0
- package/ralph/verify/review.sh +164 -0
- package/ralph/verify/tests.sh +81 -0
- package/ralph/verify.sh +224 -0
- package/templates/PROMPT.md +235 -0
- package/templates/config/fullstack.json +86 -0
- package/templates/config/go.json +81 -0
- package/templates/config/minimal.json +76 -0
- package/templates/config/node.json +81 -0
- package/templates/config/python.json +81 -0
- package/templates/config/rust.json +81 -0
- package/templates/examples/CLAUDE-django.md +174 -0
- package/templates/examples/CLAUDE-fastapi.md +270 -0
- package/templates/examples/CLAUDE-fastmcp.md +352 -0
- package/templates/examples/CLAUDE-fullstack.md +256 -0
- package/templates/examples/CLAUDE-node.md +246 -0
- package/templates/examples/CLAUDE-react.md +138 -0
- package/templates/optional/cursorrules.template +147 -0
- package/templates/optional/eslint.config.js +34 -0
- package/templates/optional/lint-staged.config.js +34 -0
- package/templates/optional/ruff.toml +125 -0
- package/templates/optional/vibe-check.yml +116 -0
- package/templates/optional/vscode-settings.json +127 -0
- package/templates/signs.json +46 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# CLAUDE.md - FastAPI Project
|
|
2
|
+
|
|
3
|
+
## Naming Conventions
|
|
4
|
+
- **Files**: `snake_case.py` — e.g., `user_service.py`, `auth_router.py`
|
|
5
|
+
- **Functions/Variables**: `snake_case` — e.g., `get_user_by_id`, `is_valid`
|
|
6
|
+
- **Classes**: `PascalCase` — e.g., `UserService`, `AuthRouter`
|
|
7
|
+
- **Pydantic Models**: `PascalCase` — e.g., `UserCreate`, `UserResponse`
|
|
8
|
+
- **Constants**: `SCREAMING_SNAKE` — e.g., `MAX_RETRIES`, `DEFAULT_LIMIT`
|
|
9
|
+
- **Database tables**: `snake_case` — e.g., `user_sessions`, `api_keys`
|
|
10
|
+
- **API endpoints**: `kebab-case` — e.g., `/api/user-profile`, `/api/v1/auth`
|
|
11
|
+
|
|
12
|
+
## Project Overview
|
|
13
|
+
|
|
14
|
+
This is a FastAPI backend application with async SQLAlchemy and Pydantic.
|
|
15
|
+
|
|
16
|
+
## Tech Stack
|
|
17
|
+
|
|
18
|
+
- **Framework**: FastAPI
|
|
19
|
+
- **ORM**: SQLAlchemy 2.0 (async)
|
|
20
|
+
- **Validation**: Pydantic v2
|
|
21
|
+
- **Database**: PostgreSQL
|
|
22
|
+
- **Migrations**: Alembic
|
|
23
|
+
- **Testing**: pytest + httpx
|
|
24
|
+
- **Task Queue**: Celery + Redis (if applicable)
|
|
25
|
+
|
|
26
|
+
## Project Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
app/
|
|
30
|
+
├── main.py # FastAPI app entry point
|
|
31
|
+
├── config.py # Settings via pydantic-settings
|
|
32
|
+
├── database.py # Async SQLAlchemy setup
|
|
33
|
+
├── models/ # SQLAlchemy models
|
|
34
|
+
├── schemas/ # Pydantic schemas
|
|
35
|
+
├── routers/ # API route handlers
|
|
36
|
+
├── services/ # Business logic
|
|
37
|
+
├── dependencies.py # FastAPI dependencies
|
|
38
|
+
└── exceptions.py # Custom exceptions
|
|
39
|
+
tests/
|
|
40
|
+
├── conftest.py # Fixtures
|
|
41
|
+
├── test_api/ # API tests
|
|
42
|
+
└── test_services/ # Unit tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Development
|
|
49
|
+
uvicorn app.main:app --reload --port 8000
|
|
50
|
+
|
|
51
|
+
# Database
|
|
52
|
+
alembic upgrade head # Run migrations
|
|
53
|
+
alembic revision --autogenerate -m "" # Create migration
|
|
54
|
+
|
|
55
|
+
# Testing
|
|
56
|
+
pytest # Run all tests
|
|
57
|
+
pytest -x # Stop on first failure
|
|
58
|
+
pytest --cov=app # With coverage
|
|
59
|
+
|
|
60
|
+
# Linting
|
|
61
|
+
ruff check app/
|
|
62
|
+
ruff format app/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Code Standards
|
|
66
|
+
|
|
67
|
+
### API Endpoints
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Good - explicit status codes, response model, dependencies
|
|
71
|
+
@router.post("/users", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
|
|
72
|
+
async def create_user(
|
|
73
|
+
user_in: UserCreate,
|
|
74
|
+
db: AsyncSession = Depends(get_db),
|
|
75
|
+
current_user: User = Depends(get_current_user),
|
|
76
|
+
) -> UserResponse:
|
|
77
|
+
"""Create a new user."""
|
|
78
|
+
return await user_service.create(db, user_in)
|
|
79
|
+
|
|
80
|
+
# Bad - implicit everything
|
|
81
|
+
@router.post("/users")
|
|
82
|
+
async def create_user(user_in: UserCreate, db = Depends(get_db)):
|
|
83
|
+
return await create(db, user_in)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Pydantic Schemas
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Good - explicit validation, examples, field descriptions
|
|
90
|
+
class UserCreate(BaseModel):
|
|
91
|
+
email: EmailStr = Field(..., description="User's email address")
|
|
92
|
+
password: str = Field(..., min_length=8, description="Password (min 8 chars)")
|
|
93
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
94
|
+
|
|
95
|
+
model_config = ConfigDict(
|
|
96
|
+
json_schema_extra={
|
|
97
|
+
"example": {
|
|
98
|
+
"email": "user@example.com",
|
|
99
|
+
"password": "securepass123",
|
|
100
|
+
"name": "John Doe"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Bad - no validation
|
|
106
|
+
class UserCreate(BaseModel):
|
|
107
|
+
email: str
|
|
108
|
+
password: str
|
|
109
|
+
name: str
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### SQLAlchemy Models
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Good - explicit types, relationships, indexes
|
|
116
|
+
class User(Base):
|
|
117
|
+
__tablename__ = "users"
|
|
118
|
+
|
|
119
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
120
|
+
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
121
|
+
hashed_password: Mapped[str] = mapped_column(String(255))
|
|
122
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
123
|
+
created_at: Mapped[datetime] = mapped_column(default=func.now())
|
|
124
|
+
|
|
125
|
+
# Relationships
|
|
126
|
+
posts: Mapped[list["Post"]] = relationship(back_populates="author")
|
|
127
|
+
|
|
128
|
+
# Bad - old style, no types
|
|
129
|
+
class User(Base):
|
|
130
|
+
__tablename__ = "users"
|
|
131
|
+
id = Column(Integer, primary_key=True)
|
|
132
|
+
email = Column(String)
|
|
133
|
+
password = Column(String)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Async Database Sessions
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Good - async context manager
|
|
140
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
141
|
+
async with async_session() as session:
|
|
142
|
+
try:
|
|
143
|
+
yield session
|
|
144
|
+
await session.commit()
|
|
145
|
+
except Exception:
|
|
146
|
+
await session.rollback()
|
|
147
|
+
raise
|
|
148
|
+
|
|
149
|
+
# Bad - no cleanup
|
|
150
|
+
async def get_db():
|
|
151
|
+
return async_session()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Error Handling
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Good - custom exceptions with proper HTTP codes
|
|
158
|
+
class NotFoundError(Exception):
|
|
159
|
+
def __init__(self, resource: str, id: int):
|
|
160
|
+
self.resource = resource
|
|
161
|
+
self.id = id
|
|
162
|
+
|
|
163
|
+
@app.exception_handler(NotFoundError)
|
|
164
|
+
async def not_found_handler(request: Request, exc: NotFoundError):
|
|
165
|
+
return JSONResponse(
|
|
166
|
+
status_code=404,
|
|
167
|
+
content={"detail": f"{exc.resource} with id {exc.id} not found"}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Bad - generic exceptions
|
|
171
|
+
raise Exception("User not found")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Service Layer
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# Good - service handles business logic
|
|
178
|
+
class UserService:
|
|
179
|
+
async def create(self, db: AsyncSession, user_in: UserCreate) -> User:
|
|
180
|
+
# Check if exists
|
|
181
|
+
existing = await self.get_by_email(db, user_in.email)
|
|
182
|
+
if existing:
|
|
183
|
+
raise ConflictError("User", "email", user_in.email)
|
|
184
|
+
|
|
185
|
+
# Hash password
|
|
186
|
+
hashed = hash_password(user_in.password)
|
|
187
|
+
|
|
188
|
+
# Create user
|
|
189
|
+
user = User(email=user_in.email, hashed_password=hashed, name=user_in.name)
|
|
190
|
+
db.add(user)
|
|
191
|
+
await db.flush()
|
|
192
|
+
return user
|
|
193
|
+
|
|
194
|
+
# Bad - logic in router
|
|
195
|
+
@router.post("/users")
|
|
196
|
+
async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
|
|
197
|
+
existing = await db.execute(select(User).where(User.email == user_in.email))
|
|
198
|
+
if existing.scalar():
|
|
199
|
+
raise HTTPException(409, "Email exists")
|
|
200
|
+
hashed = bcrypt.hash(user_in.password)
|
|
201
|
+
user = User(email=user_in.email, hashed_password=hashed)
|
|
202
|
+
db.add(user)
|
|
203
|
+
# ... more logic
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Do NOT
|
|
207
|
+
|
|
208
|
+
- Use `Any` type - create proper Pydantic models
|
|
209
|
+
- Put business logic in routers - use service layer
|
|
210
|
+
- Use synchronous database calls in async endpoints
|
|
211
|
+
- Hardcode secrets - use `pydantic-settings` with env vars
|
|
212
|
+
- Skip input validation - use Pydantic Field validators
|
|
213
|
+
- Return SQLAlchemy models directly - use response schemas
|
|
214
|
+
- Use `*` imports - explicit imports only
|
|
215
|
+
- Catch generic `Exception` - catch specific exceptions
|
|
216
|
+
|
|
217
|
+
## Do
|
|
218
|
+
|
|
219
|
+
- Use async/await consistently
|
|
220
|
+
- Add OpenAPI descriptions to all endpoints
|
|
221
|
+
- Use dependency injection for services
|
|
222
|
+
- Write tests for all endpoints
|
|
223
|
+
- Use Alembic for all schema changes
|
|
224
|
+
- Add proper logging with structlog
|
|
225
|
+
- Use HTTPException for API errors
|
|
226
|
+
- Validate all inputs with Pydantic
|
|
227
|
+
|
|
228
|
+
## Environment Variables
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/dbname
|
|
232
|
+
SECRET_KEY=your-secret-key
|
|
233
|
+
REDIS_URL=redis://localhost:6379/0
|
|
234
|
+
DEBUG=false
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Always use `pydantic-settings`:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
class Settings(BaseSettings):
|
|
241
|
+
database_url: PostgresDsn
|
|
242
|
+
secret_key: str
|
|
243
|
+
debug: bool = False
|
|
244
|
+
|
|
245
|
+
model_config = SettingsConfigDict(env_file=".env")
|
|
246
|
+
|
|
247
|
+
settings = Settings()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
# Good - async test with fixtures
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
async def test_create_user(client: AsyncClient, db: AsyncSession):
|
|
256
|
+
response = await client.post(
|
|
257
|
+
"/api/users",
|
|
258
|
+
json={"email": "test@example.com", "password": "testpass123", "name": "Test"}
|
|
259
|
+
)
|
|
260
|
+
assert response.status_code == 201
|
|
261
|
+
data = response.json()
|
|
262
|
+
assert data["email"] == "test@example.com"
|
|
263
|
+
assert "password" not in data # Never return password
|
|
264
|
+
|
|
265
|
+
# conftest.py
|
|
266
|
+
@pytest.fixture
|
|
267
|
+
async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
|
|
268
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
269
|
+
yield client
|
|
270
|
+
```
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# CLAUDE.md - FastMCP Server
|
|
2
|
+
|
|
3
|
+
## Naming Conventions
|
|
4
|
+
- **Files**: `snake_case.py` — e.g., `search_tool.py`, `db_resource.py`
|
|
5
|
+
- **Functions/Variables**: `snake_case` — e.g., `search_files`, `is_valid`
|
|
6
|
+
- **Classes**: `PascalCase` — e.g., `SearchResult`, `FileResource`
|
|
7
|
+
- **Tool names**: `snake_case` — e.g., `search_files`, `read_database`
|
|
8
|
+
- **Resource URIs**: `kebab-case` — e.g., `file://project-files`, `db://user-data`
|
|
9
|
+
- **Constants**: `SCREAMING_SNAKE` — e.g., `MAX_RESULTS`, `DEFAULT_TIMEOUT`
|
|
10
|
+
|
|
11
|
+
## Project Overview
|
|
12
|
+
|
|
13
|
+
This is an MCP (Model Context Protocol) server built with FastMCP. It exposes tools, resources, and prompts to LLM clients.
|
|
14
|
+
|
|
15
|
+
## Tech Stack
|
|
16
|
+
|
|
17
|
+
- **Framework**: FastMCP 2.0
|
|
18
|
+
- **Protocol**: Model Context Protocol (MCP)
|
|
19
|
+
- **Validation**: Pydantic v2
|
|
20
|
+
- **Async**: anyio
|
|
21
|
+
|
|
22
|
+
## Project Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
server/
|
|
26
|
+
├── main.py # FastMCP app entry point
|
|
27
|
+
├── tools/ # Tool implementations
|
|
28
|
+
│ ├── __init__.py
|
|
29
|
+
│ └── search.py
|
|
30
|
+
├── resources/ # Resource handlers
|
|
31
|
+
│ ├── __init__.py
|
|
32
|
+
│ └── data.py
|
|
33
|
+
├── prompts/ # Prompt templates
|
|
34
|
+
│ ├── __init__.py
|
|
35
|
+
│ └── analysis.py
|
|
36
|
+
├── dependencies.py # Dependency injection
|
|
37
|
+
└── config.py # Settings
|
|
38
|
+
tests/
|
|
39
|
+
├── conftest.py
|
|
40
|
+
└── test_tools.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Development
|
|
47
|
+
fastmcp dev server/main.py
|
|
48
|
+
|
|
49
|
+
# Run server
|
|
50
|
+
python server/main.py
|
|
51
|
+
|
|
52
|
+
# Testing
|
|
53
|
+
pytest
|
|
54
|
+
|
|
55
|
+
# Install to Claude Desktop
|
|
56
|
+
fastmcp install server/main.py --name "My Server"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Code Standards
|
|
60
|
+
|
|
61
|
+
### Basic Server Setup
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Good - clear name, proper structure
|
|
65
|
+
from fastmcp import FastMCP
|
|
66
|
+
|
|
67
|
+
mcp = FastMCP("My Server")
|
|
68
|
+
|
|
69
|
+
@mcp.tool
|
|
70
|
+
def search(query: str) -> list[dict]:
|
|
71
|
+
"""Search for items matching the query."""
|
|
72
|
+
return [{"id": 1, "name": "Result"}]
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
mcp.run()
|
|
76
|
+
|
|
77
|
+
# Bad - no name, no types
|
|
78
|
+
from fastmcp import FastMCP
|
|
79
|
+
mcp = FastMCP()
|
|
80
|
+
|
|
81
|
+
@mcp.tool
|
|
82
|
+
def search(query):
|
|
83
|
+
return [{"id": 1}]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Tools
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from typing import Annotated
|
|
90
|
+
from pydantic import Field
|
|
91
|
+
from fastmcp import FastMCP
|
|
92
|
+
from fastmcp.exceptions import ToolError
|
|
93
|
+
|
|
94
|
+
mcp = FastMCP("My Server")
|
|
95
|
+
|
|
96
|
+
# Good - typed parameters, validation, docstring, error handling
|
|
97
|
+
@mcp.tool
|
|
98
|
+
async def search_products(
|
|
99
|
+
query: Annotated[str, Field(min_length=1, description="Search query")],
|
|
100
|
+
max_results: Annotated[int, Field(ge=1, le=100, description="Max results")] = 10,
|
|
101
|
+
category: str | None = None,
|
|
102
|
+
) -> list[dict]:
|
|
103
|
+
"""Search the product catalog.
|
|
104
|
+
|
|
105
|
+
Returns matching products with id, name, and price.
|
|
106
|
+
"""
|
|
107
|
+
if not query.strip():
|
|
108
|
+
raise ToolError("Query cannot be empty")
|
|
109
|
+
|
|
110
|
+
results = await fetch_products(query, max_results, category)
|
|
111
|
+
return results
|
|
112
|
+
|
|
113
|
+
# Bad - no types, no validation, no docstring
|
|
114
|
+
@mcp.tool
|
|
115
|
+
def search_products(query, max_results=10, **kwargs):
|
|
116
|
+
return fetch_products(query, max_results)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Resources
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from fastmcp import FastMCP
|
|
123
|
+
from fastmcp.resources import TextResource, FileResource
|
|
124
|
+
from fastmcp.exceptions import ResourceError
|
|
125
|
+
from pathlib import Path
|
|
126
|
+
|
|
127
|
+
mcp = FastMCP("My Server")
|
|
128
|
+
|
|
129
|
+
# Good - static resource with proper URI and MIME type
|
|
130
|
+
config_resource = TextResource(
|
|
131
|
+
uri="config://app/settings",
|
|
132
|
+
name="App Settings",
|
|
133
|
+
text='{"theme": "dark", "version": "1.0"}',
|
|
134
|
+
mime_type="application/json"
|
|
135
|
+
)
|
|
136
|
+
mcp.add_resource(config_resource)
|
|
137
|
+
|
|
138
|
+
# Good - dynamic resource with template
|
|
139
|
+
@mcp.resource("users://{user_id}/profile")
|
|
140
|
+
async def get_user_profile(user_id: str) -> dict:
|
|
141
|
+
"""Get user profile by ID."""
|
|
142
|
+
user = await fetch_user(user_id)
|
|
143
|
+
if not user:
|
|
144
|
+
raise ResourceError(f"User {user_id} not found")
|
|
145
|
+
return user
|
|
146
|
+
|
|
147
|
+
# Bad - no URI scheme, no error handling
|
|
148
|
+
@mcp.resource("user")
|
|
149
|
+
def get_user(id):
|
|
150
|
+
return fetch_user(id)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Prompts
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from fastmcp import FastMCP
|
|
157
|
+
from fastmcp.prompts import PromptMessage
|
|
158
|
+
|
|
159
|
+
mcp = FastMCP("My Server")
|
|
160
|
+
|
|
161
|
+
# Good - typed parameters, clear docstring, structured return
|
|
162
|
+
@mcp.prompt
|
|
163
|
+
def analyze_code(
|
|
164
|
+
code: str,
|
|
165
|
+
language: str = "python",
|
|
166
|
+
focus: str = "security"
|
|
167
|
+
) -> list[PromptMessage]:
|
|
168
|
+
"""Analyze code for issues.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
code: The code to analyze
|
|
172
|
+
language: Programming language
|
|
173
|
+
focus: Analysis focus (security, performance, style)
|
|
174
|
+
"""
|
|
175
|
+
return [
|
|
176
|
+
PromptMessage(
|
|
177
|
+
role="user",
|
|
178
|
+
content=f"Analyze this {language} code for {focus} issues:\n\n```{language}\n{code}\n```"
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# Bad - no types, returns plain string
|
|
183
|
+
@mcp.prompt
|
|
184
|
+
def analyze(code):
|
|
185
|
+
return f"Analyze: {code}"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Dependency Injection
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from fastmcp import FastMCP
|
|
192
|
+
from fastmcp.server.dependencies import get_context
|
|
193
|
+
|
|
194
|
+
mcp = FastMCP("My Server")
|
|
195
|
+
|
|
196
|
+
# Good - hide runtime values from LLM
|
|
197
|
+
def get_api_key() -> str:
|
|
198
|
+
return os.environ["API_KEY"]
|
|
199
|
+
|
|
200
|
+
def get_db() -> Database:
|
|
201
|
+
return Database(os.environ["DATABASE_URL"])
|
|
202
|
+
|
|
203
|
+
@mcp.tool
|
|
204
|
+
async def fetch_data(
|
|
205
|
+
query: str,
|
|
206
|
+
api_key: str = Depends(get_api_key), # Hidden from schema
|
|
207
|
+
db: Database = Depends(get_db), # Hidden from schema
|
|
208
|
+
) -> dict:
|
|
209
|
+
"""Fetch data from external API."""
|
|
210
|
+
return await db.query(query)
|
|
211
|
+
|
|
212
|
+
# Bad - exposing secrets in schema
|
|
213
|
+
@mcp.tool
|
|
214
|
+
def fetch_data(query: str, api_key: str) -> dict:
|
|
215
|
+
return call_api(query, api_key)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Error Handling
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from fastmcp.exceptions import ToolError, ResourceError
|
|
222
|
+
|
|
223
|
+
# Good - specific errors with helpful messages
|
|
224
|
+
@mcp.tool
|
|
225
|
+
def divide(a: float, b: float) -> float:
|
|
226
|
+
"""Divide two numbers."""
|
|
227
|
+
if b == 0:
|
|
228
|
+
raise ToolError("Cannot divide by zero")
|
|
229
|
+
return a / b
|
|
230
|
+
|
|
231
|
+
@mcp.resource("data://{id}")
|
|
232
|
+
def get_data(id: str) -> dict:
|
|
233
|
+
"""Get data by ID."""
|
|
234
|
+
data = fetch_data(id)
|
|
235
|
+
if not data:
|
|
236
|
+
raise ResourceError(f"Data with ID '{id}' not found")
|
|
237
|
+
return data
|
|
238
|
+
|
|
239
|
+
# Bad - generic exceptions
|
|
240
|
+
@mcp.tool
|
|
241
|
+
def divide(a, b):
|
|
242
|
+
return a / b # ZeroDivisionError not handled
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Async Best Practices
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
import anyio
|
|
249
|
+
|
|
250
|
+
# Good - async for I/O operations
|
|
251
|
+
@mcp.tool
|
|
252
|
+
async def fetch_url(url: str) -> str:
|
|
253
|
+
"""Fetch content from URL."""
|
|
254
|
+
async with httpx.AsyncClient() as client:
|
|
255
|
+
response = await client.get(url)
|
|
256
|
+
return response.text
|
|
257
|
+
|
|
258
|
+
# Good - wrap CPU-bound sync code
|
|
259
|
+
@mcp.tool
|
|
260
|
+
async def process_image(image_data: bytes) -> bytes:
|
|
261
|
+
"""Process image (CPU-intensive)."""
|
|
262
|
+
return await anyio.to_thread.run_sync(
|
|
263
|
+
lambda: heavy_image_processing(image_data)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Bad - blocking call in async context
|
|
267
|
+
@mcp.tool
|
|
268
|
+
async def fetch_url(url: str) -> str:
|
|
269
|
+
return requests.get(url).text # Blocks event loop!
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Do NOT
|
|
273
|
+
|
|
274
|
+
- Use `*args` or `**kwargs` in tools - FastMCP needs complete parameter schemas
|
|
275
|
+
- Expose secrets as tool parameters - use `Depends()` for injection
|
|
276
|
+
- Use blocking I/O in async functions - wrap with `anyio.to_thread.run_sync()`
|
|
277
|
+
- Skip type hints - they generate the JSON schema for clients
|
|
278
|
+
- Catch and silence exceptions - use `ToolError`/`ResourceError` with helpful messages
|
|
279
|
+
- Use generic names like `data` or `process` - be specific
|
|
280
|
+
|
|
281
|
+
## Do
|
|
282
|
+
|
|
283
|
+
- Add docstrings to all tools, resources, and prompts
|
|
284
|
+
- Use `Annotated` with `Field` for parameter validation and descriptions
|
|
285
|
+
- Use async for all I/O operations
|
|
286
|
+
- Use `Depends()` to inject runtime values (API keys, DB connections)
|
|
287
|
+
- Use specific error types (`ToolError`, `ResourceError`)
|
|
288
|
+
- Add proper MIME types to resources
|
|
289
|
+
- Use URI schemes (`data://`, `config://`, `file://`)
|
|
290
|
+
- Test tools with `mcp.call_tool()` in pytest
|
|
291
|
+
|
|
292
|
+
## Testing
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
import pytest
|
|
296
|
+
from server.main import mcp
|
|
297
|
+
|
|
298
|
+
@pytest.mark.asyncio
|
|
299
|
+
async def test_search_tool():
|
|
300
|
+
result = await mcp.call_tool("search_products", {"query": "test"})
|
|
301
|
+
assert isinstance(result, list)
|
|
302
|
+
|
|
303
|
+
@pytest.mark.asyncio
|
|
304
|
+
async def test_search_empty_query():
|
|
305
|
+
with pytest.raises(ToolError, match="cannot be empty"):
|
|
306
|
+
await mcp.call_tool("search_products", {"query": ""})
|
|
307
|
+
|
|
308
|
+
@pytest.mark.asyncio
|
|
309
|
+
async def test_user_resource():
|
|
310
|
+
result = await mcp.read_resource("users://123/profile")
|
|
311
|
+
assert "name" in result
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Configuration
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
from pydantic_settings import BaseSettings
|
|
318
|
+
|
|
319
|
+
class Settings(BaseSettings):
|
|
320
|
+
api_key: str
|
|
321
|
+
database_url: str
|
|
322
|
+
debug: bool = False
|
|
323
|
+
|
|
324
|
+
model_config = {"env_file": ".env"}
|
|
325
|
+
|
|
326
|
+
settings = Settings()
|
|
327
|
+
|
|
328
|
+
# Use in server
|
|
329
|
+
mcp = FastMCP(
|
|
330
|
+
"My Server",
|
|
331
|
+
debug=settings.debug,
|
|
332
|
+
mask_error_details=not settings.debug, # Hide errors in production
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Deployment
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
# FastMCP Cloud (free for personal)
|
|
340
|
+
fastmcp deploy server/main.py
|
|
341
|
+
|
|
342
|
+
# Install to Claude Desktop
|
|
343
|
+
fastmcp install server/main.py --name "My Server"
|
|
344
|
+
|
|
345
|
+
# HTTP server
|
|
346
|
+
uvicorn server.main:mcp.http_app --host 0.0.0.0 --port 8000
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Resources
|
|
350
|
+
|
|
351
|
+
- [FastMCP Docs](https://gofastmcp.com)
|
|
352
|
+
- [MCP Specification](https://modelcontextprotocol.io)
|