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.
Files changed (164) hide show
  1. package/.claude/commands/explain.md +114 -0
  2. package/.claude/commands/idea.md +370 -0
  3. package/.claude/commands/my-dna.md +122 -0
  4. package/.claude/commands/prd.md +286 -0
  5. package/.claude/commands/review.md +167 -0
  6. package/.claude/commands/sign.md +32 -0
  7. package/.claude/commands/styleguide.md +450 -0
  8. package/.claude/commands/tour.md +301 -0
  9. package/.claude/commands/vibe-check.md +116 -0
  10. package/.claude/commands/vibe-help.md +47 -0
  11. package/.claude/commands/vibe-list.md +203 -0
  12. package/.claude/settings.json +75 -0
  13. package/.claude/settings.local.json +12 -0
  14. package/.pre-commit-hooks.yaml +102 -0
  15. package/LICENSE +21 -0
  16. package/README.md +214 -0
  17. package/bin/postinstall.sh +29 -0
  18. package/bin/ralph.sh +171 -0
  19. package/bin/thrivekit.sh +24 -0
  20. package/bin/vibe-check.js +19 -0
  21. package/dist/checks/check-any-types.d.ts +6 -0
  22. package/dist/checks/check-any-types.d.ts.map +1 -0
  23. package/dist/checks/check-any-types.js +73 -0
  24. package/dist/checks/check-any-types.js.map +1 -0
  25. package/dist/checks/check-commented-code.d.ts +6 -0
  26. package/dist/checks/check-commented-code.d.ts.map +1 -0
  27. package/dist/checks/check-commented-code.js +81 -0
  28. package/dist/checks/check-commented-code.js.map +1 -0
  29. package/dist/checks/check-console-error.d.ts +6 -0
  30. package/dist/checks/check-console-error.d.ts.map +1 -0
  31. package/dist/checks/check-console-error.js +41 -0
  32. package/dist/checks/check-console-error.js.map +1 -0
  33. package/dist/checks/check-debug-statements.d.ts +6 -0
  34. package/dist/checks/check-debug-statements.d.ts.map +1 -0
  35. package/dist/checks/check-debug-statements.js +120 -0
  36. package/dist/checks/check-debug-statements.js.map +1 -0
  37. package/dist/checks/check-deep-nesting.d.ts +6 -0
  38. package/dist/checks/check-deep-nesting.d.ts.map +1 -0
  39. package/dist/checks/check-deep-nesting.js +116 -0
  40. package/dist/checks/check-deep-nesting.js.map +1 -0
  41. package/dist/checks/check-docker-platform.d.ts +6 -0
  42. package/dist/checks/check-docker-platform.d.ts.map +1 -0
  43. package/dist/checks/check-docker-platform.js +42 -0
  44. package/dist/checks/check-docker-platform.js.map +1 -0
  45. package/dist/checks/check-dry-violations.d.ts +6 -0
  46. package/dist/checks/check-dry-violations.d.ts.map +1 -0
  47. package/dist/checks/check-dry-violations.js +124 -0
  48. package/dist/checks/check-dry-violations.js.map +1 -0
  49. package/dist/checks/check-empty-catch.d.ts +6 -0
  50. package/dist/checks/check-empty-catch.d.ts.map +1 -0
  51. package/dist/checks/check-empty-catch.js +111 -0
  52. package/dist/checks/check-empty-catch.js.map +1 -0
  53. package/dist/checks/check-function-length.d.ts +6 -0
  54. package/dist/checks/check-function-length.d.ts.map +1 -0
  55. package/dist/checks/check-function-length.js +152 -0
  56. package/dist/checks/check-function-length.js.map +1 -0
  57. package/dist/checks/check-hardcoded-ai-models.d.ts +10 -0
  58. package/dist/checks/check-hardcoded-ai-models.d.ts.map +1 -0
  59. package/dist/checks/check-hardcoded-ai-models.js +102 -0
  60. package/dist/checks/check-hardcoded-ai-models.js.map +1 -0
  61. package/dist/checks/check-hardcoded-urls.d.ts +6 -0
  62. package/dist/checks/check-hardcoded-urls.d.ts.map +1 -0
  63. package/dist/checks/check-hardcoded-urls.js +124 -0
  64. package/dist/checks/check-hardcoded-urls.js.map +1 -0
  65. package/dist/checks/check-magic-numbers.d.ts +6 -0
  66. package/dist/checks/check-magic-numbers.d.ts.map +1 -0
  67. package/dist/checks/check-magic-numbers.js +116 -0
  68. package/dist/checks/check-magic-numbers.js.map +1 -0
  69. package/dist/checks/check-secrets.d.ts +6 -0
  70. package/dist/checks/check-secrets.d.ts.map +1 -0
  71. package/dist/checks/check-secrets.js +138 -0
  72. package/dist/checks/check-secrets.js.map +1 -0
  73. package/dist/checks/check-snake-case-ts.d.ts +6 -0
  74. package/dist/checks/check-snake-case-ts.d.ts.map +1 -0
  75. package/dist/checks/check-snake-case-ts.js +78 -0
  76. package/dist/checks/check-snake-case-ts.js.map +1 -0
  77. package/dist/checks/check-todo-fixme.d.ts +6 -0
  78. package/dist/checks/check-todo-fixme.d.ts.map +1 -0
  79. package/dist/checks/check-todo-fixme.js +41 -0
  80. package/dist/checks/check-todo-fixme.js.map +1 -0
  81. package/dist/checks/check-unsafe-html.d.ts +6 -0
  82. package/dist/checks/check-unsafe-html.d.ts.map +1 -0
  83. package/dist/checks/check-unsafe-html.js +101 -0
  84. package/dist/checks/check-unsafe-html.js.map +1 -0
  85. package/dist/checks/index.d.ts +30 -0
  86. package/dist/checks/index.d.ts.map +1 -0
  87. package/dist/checks/index.js +57 -0
  88. package/dist/checks/index.js.map +1 -0
  89. package/dist/cli.d.ts +13 -0
  90. package/dist/cli.d.ts.map +1 -0
  91. package/dist/cli.js +206 -0
  92. package/dist/cli.js.map +1 -0
  93. package/dist/index.d.ts +9 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +10 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/utils/file-reader.d.ts +24 -0
  98. package/dist/utils/file-reader.d.ts.map +1 -0
  99. package/dist/utils/file-reader.js +140 -0
  100. package/dist/utils/file-reader.js.map +1 -0
  101. package/dist/utils/patterns.d.ts +27 -0
  102. package/dist/utils/patterns.d.ts.map +1 -0
  103. package/dist/utils/patterns.js +84 -0
  104. package/dist/utils/patterns.js.map +1 -0
  105. package/dist/utils/reporters.d.ts +21 -0
  106. package/dist/utils/reporters.d.ts.map +1 -0
  107. package/dist/utils/reporters.js +115 -0
  108. package/dist/utils/reporters.js.map +1 -0
  109. package/dist/utils/types.d.ts +71 -0
  110. package/dist/utils/types.d.ts.map +1 -0
  111. package/dist/utils/types.js +5 -0
  112. package/dist/utils/types.js.map +1 -0
  113. package/package.json +82 -0
  114. package/ralph/api.sh +210 -0
  115. package/ralph/backup.sh +838 -0
  116. package/ralph/browser-verify/README.md +135 -0
  117. package/ralph/browser-verify/verify.ts +450 -0
  118. package/ralph/checks/check-fastapi-responses.py +155 -0
  119. package/ralph/hooks/hooks-config.json +72 -0
  120. package/ralph/hooks/inject-context.sh +44 -0
  121. package/ralph/hooks/install.sh +207 -0
  122. package/ralph/hooks/log-tools.sh +45 -0
  123. package/ralph/hooks/protect-prd.sh +27 -0
  124. package/ralph/hooks/save-learnings.sh +36 -0
  125. package/ralph/hooks/warn-debug.sh +54 -0
  126. package/ralph/hooks/warn-empty-catch.sh +63 -0
  127. package/ralph/hooks/warn-secrets.sh +89 -0
  128. package/ralph/hooks/warn-urls.sh +77 -0
  129. package/ralph/init.sh +388 -0
  130. package/ralph/loop.sh +570 -0
  131. package/ralph/playwright.sh +238 -0
  132. package/ralph/prd.sh +295 -0
  133. package/ralph/setup/feature-tour.sh +155 -0
  134. package/ralph/setup/quick-setup.sh +239 -0
  135. package/ralph/setup/tutorial.sh +159 -0
  136. package/ralph/setup/ui.sh +136 -0
  137. package/ralph/setup.sh +353 -0
  138. package/ralph/signs.sh +150 -0
  139. package/ralph/utils.sh +682 -0
  140. package/ralph/verify/browser.sh +324 -0
  141. package/ralph/verify/lint.sh +363 -0
  142. package/ralph/verify/review.sh +164 -0
  143. package/ralph/verify/tests.sh +81 -0
  144. package/ralph/verify.sh +224 -0
  145. package/templates/PROMPT.md +235 -0
  146. package/templates/config/fullstack.json +86 -0
  147. package/templates/config/go.json +81 -0
  148. package/templates/config/minimal.json +76 -0
  149. package/templates/config/node.json +81 -0
  150. package/templates/config/python.json +81 -0
  151. package/templates/config/rust.json +81 -0
  152. package/templates/examples/CLAUDE-django.md +174 -0
  153. package/templates/examples/CLAUDE-fastapi.md +270 -0
  154. package/templates/examples/CLAUDE-fastmcp.md +352 -0
  155. package/templates/examples/CLAUDE-fullstack.md +256 -0
  156. package/templates/examples/CLAUDE-node.md +246 -0
  157. package/templates/examples/CLAUDE-react.md +138 -0
  158. package/templates/optional/cursorrules.template +147 -0
  159. package/templates/optional/eslint.config.js +34 -0
  160. package/templates/optional/lint-staged.config.js +34 -0
  161. package/templates/optional/ruff.toml +125 -0
  162. package/templates/optional/vibe-check.yml +116 -0
  163. package/templates/optional/vscode-settings.json +127 -0
  164. 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)