Coverage for fastblocks/mcp/resources.py: 0%

51 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

1"""MCP resources for FastBlocks schemas, documentation, and patterns.""" 

2 

3import logging 

4from typing import Any 

5 

6from acb.depends import depends 

7 

8logger = logging.getLogger(__name__) 

9 

10 

11# Template Resources 

12 

13 

14async def get_template_syntax_reference() -> dict[str, Any]: 

15 """Get FastBlocks template syntax reference. 

16 

17 Returns: 

18 Dict with comprehensive syntax documentation 

19 """ 

20 return { 

21 "name": "FastBlocks Template Syntax Reference", 

22 "version": "0.16.0", 

23 "delimiters": { 

24 "variable": { 

25 "open": "[[", 

26 "close": "]]", 

27 "description": "Output variable values", 

28 "examples": [ 

29 "[[ user.name ]]", 

30 "[[ items|length ]]", 

31 "[[ price|currency ]]", 

32 ], 

33 }, 

34 "statement": { 

35 "open": "[%", 

36 "close": "%]", 

37 "description": "Control flow and logic", 

38 "examples": [ 

39 "[% if user.is_active %]", 

40 "[% for item in items %]", 

41 "[% block content %]", 

42 ], 

43 }, 

44 "comment": { 

45 "open": "[#", 

46 "close": "#]", 

47 "description": "Template comments (not rendered)", 

48 "examples": ["[# TODO: Add user avatar #]"], 

49 }, 

50 }, 

51 "common_patterns": { 

52 "conditionals": { 

53 "if": "[% if condition %]...[% endif %]", 

54 "if_else": "[% if condition %]...[% else %]...[% endif %]", 

55 "if_elif": "[% if cond1 %]...[% elif cond2 %]...[% else %]...[% endif %]", 

56 }, 

57 "loops": { 

58 "for": "[% for item in items %]...[% endfor %]", 

59 "for_with_index": "[% for idx, item in enumerate(items) %]...[% endfor %]", 

60 "loop_controls": "loop.index, loop.index0, loop.first, loop.last", 

61 }, 

62 "blocks": { 

63 "define": "[% block name %]...[% endblock %]", 

64 "override": "Child templates can override parent blocks", 

65 "super": "[[ super() ]] calls parent block content", 

66 }, 

67 "includes": { 

68 "basic": "[% include 'path/to/template.html' %]", 

69 "with_context": "[% include 'template.html' with context %]", 

70 "with_vars": "[% include 'template.html' with {'var': value} %]", 

71 }, 

72 }, 

73 "best_practices": [ 

74 "Use meaningful variable names", 

75 "Keep templates focused and small", 

76 "Extract reusable components", 

77 "Use blocks for layout inheritance", 

78 "Add comments for complex logic", 

79 ], 

80 } 

81 

82 

83async def get_available_filters() -> dict[str, Any]: 

84 """Get list of available template filters. 

85 

86 Returns: 

87 Dict with filter documentation 

88 """ 

89 return { 

90 "name": "FastBlocks Template Filters", 

91 "builtin_filters": { 

92 "string_filters": [ 

93 { 

94 "name": "upper", 

95 "description": "Convert to uppercase", 

96 "example": "[[ name|upper ]]", 

97 }, 

98 { 

99 "name": "lower", 

100 "description": "Convert to lowercase", 

101 "example": "[[ name|lower ]]", 

102 }, 

103 { 

104 "name": "title", 

105 "description": "Title case", 

106 "example": "[[ name|title ]]", 

107 }, 

108 { 

109 "name": "capitalize", 

110 "description": "Capitalize first letter", 

111 "example": "[[ name|capitalize ]]", 

112 }, 

113 { 

114 "name": "trim", 

115 "description": "Remove whitespace", 

116 "example": "[[ text|trim ]]", 

117 }, 

118 ], 

119 "number_filters": [ 

120 { 

121 "name": "round", 

122 "description": "Round to N decimal places", 

123 "example": "[[ price|round(2) ]]", 

124 }, 

125 { 

126 "name": "abs", 

127 "description": "Absolute value", 

128 "example": "[[ number|abs ]]", 

129 }, 

130 ], 

131 "list_filters": [ 

132 { 

133 "name": "length", 

134 "description": "Get list/string length", 

135 "example": "[[ items|length ]]", 

136 }, 

137 { 

138 "name": "first", 

139 "description": "Get first item", 

140 "example": "[[ items|first ]]", 

141 }, 

142 { 

143 "name": "last", 

144 "description": "Get last item", 

145 "example": "[[ items|last ]]", 

146 }, 

147 { 

148 "name": "join", 

149 "description": "Join list with separator", 

150 "example": "[[ tags|join(', ') ]]", 

151 }, 

152 ], 

153 "formatting_filters": [ 

154 { 

155 "name": "date", 

156 "description": "Format date", 

157 "example": "[[ created_at|date('%Y-%m-%d') ]]", 

158 }, 

159 { 

160 "name": "currency", 

161 "description": "Format as currency", 

162 "example": "[[ price|currency ]]", 

163 }, 

164 { 

165 "name": "truncate", 

166 "description": "Truncate string", 

167 "example": "[[ text|truncate(100) ]]", 

168 }, 

169 ], 

170 }, 

171 "fastblocks_filters": { 

172 "htmx_filters": [ 

173 { 

174 "name": "htmx_attrs", 

175 "description": "Generate HTMX attributes", 

176 "example": "[[ attrs|htmx_attrs ]]", 

177 }, 

178 ], 

179 "component_filters": [ 

180 { 

181 "name": "render_component", 

182 "description": "Render HTMY component", 

183 "example": "[[ render_component('user_card', {'name': 'John'}) ]]", 

184 }, 

185 ], 

186 }, 

187 } 

188 

189 

190async def get_htmy_component_catalog() -> dict[str, Any]: 

191 """Get catalog of available HTMY components. 

192 

193 Returns: 

194 Dict with component catalog 

195 """ 

196 try: 

197 htmy_adapter = depends.get("htmy") 

198 if htmy_adapter is None: 

199 return { 

200 "success": False, 

201 "error": "HTMY adapter not available", 

202 } 

203 

204 components = await htmy_adapter.discover_components() 

205 

206 return { 

207 "name": "HTMY Component Catalog", 

208 "components": [ 

209 { 

210 "name": name, 

211 "type": metadata.type.value, 

212 "status": metadata.status.value, 

213 "path": str(metadata.path), 

214 "description": metadata.docstring, 

215 "htmx_enabled": bool(metadata.htmx_attributes), 

216 } 

217 for name, metadata in components.items() 

218 ], 

219 "count": len(components), 

220 "component_types": { 

221 "basic": "Simple function-based components", 

222 "dataclass": "Dataclass-based components with type hints", 

223 "htmx": "HTMX-enabled interactive components", 

224 "composite": "Components composed of other components", 

225 }, 

226 } 

227 

228 except Exception as e: 

229 logger.error(f"Error getting component catalog: {e}") 

230 return {"success": False, "error": str(e)} 

231 

232 

233# Configuration Resources 

234 

235 

236async def get_adapter_schemas() -> dict[str, Any]: 

237 """Get configuration schemas for all adapters. 

238 

239 Returns: 

240 Dict with adapter configuration schemas 

241 """ 

242 try: 

243 from .discovery import AdapterDiscoveryServer 

244 

245 discovery = AdapterDiscoveryServer() 

246 adapters = await discovery.discover_adapters() 

247 

248 return { 

249 "name": "Adapter Configuration Schemas", 

250 "adapters": { 

251 name: { 

252 "name": info.name, 

253 "category": info.category, 

254 "module_path": info.module_path, 

255 "settings_class": info.settings_class, 

256 "description": info.description, 

257 } 

258 for name, info in adapters.items() 

259 }, 

260 "common_settings": { 

261 "enabled": {"type": "boolean", "default": True}, 

262 "debug": {"type": "boolean", "default": False}, 

263 "cache_enabled": {"type": "boolean", "default": True}, 

264 }, 

265 } 

266 

267 except Exception as e: 

268 logger.error(f"Error getting adapter schemas: {e}") 

269 return {"success": False, "error": str(e)} 

270 

271 

272async def get_settings_documentation() -> dict[str, Any]: 

273 """Get FastBlocks settings documentation. 

274 

275 Returns: 

276 Dict with settings structure and descriptions 

277 """ 

278 return { 

279 "name": "FastBlocks Settings Documentation", 

280 "settings_files": { 

281 "app.yml": { 

282 "description": "Application configuration", 

283 "required_fields": { 

284 "title": "Application title", 

285 "domain": "Application domain", 

286 }, 

287 "optional_fields": { 

288 "description": "Application description", 

289 "version": "Application version", 

290 }, 

291 }, 

292 "adapters.yml": { 

293 "description": "Adapter selection and configuration", 

294 "structure": { 

295 "routes": "Route handler adapter (default, custom)", 

296 "templates": "Template engine (jinja2, htmy)", 

297 "auth": "Authentication adapter (basic, jwt, oauth)", 

298 "sitemap": "Sitemap generator (asgi, native, cached)", 

299 }, 

300 }, 

301 "debug.yml": { 

302 "description": "Debug and development settings", 

303 "fields": { 

304 "fastblocks": "Enable FastBlocks debug mode", 

305 "production": "Production environment flag", 

306 }, 

307 }, 

308 }, 

309 "environment_variables": { 

310 "FASTBLOCKS_ENV": "Environment name (development, staging, production)", 

311 "FASTBLOCKS_DEBUG": "Override debug mode", 

312 "FASTBLOCKS_SECRET_KEY": "Secret key for cryptography", 

313 }, 

314 } 

315 

316 

317async def get_best_practices() -> dict[str, Any]: 

318 """Get FastBlocks best practices guide. 

319 

320 Returns: 

321 Dict with best practices documentation 

322 """ 

323 return { 

324 "name": "FastBlocks Best Practices", 

325 "architecture": { 

326 "separation_of_concerns": [ 

327 "Keep routes thin, move logic to services", 

328 "Use adapters for external integrations", 

329 "Extract reusable components", 

330 ], 

331 "template_organization": [ 

332 "Use variant-based organization (base, bulma, etc.)", 

333 "Keep blocks focused and small", 

334 "Use template inheritance for layouts", 

335 ], 

336 "component_design": [ 

337 "Prefer dataclass components for type safety", 

338 "Use HTMX components for interactivity", 

339 "Document component props and behavior", 

340 ], 

341 }, 

342 "performance": { 

343 "caching": [ 

344 "Enable template caching in production", 

345 "Use Redis for distributed caching", 

346 "Cache database queries appropriately", 

347 ], 

348 "optimization": [ 

349 "Minify CSS and JavaScript", 

350 "Use async operations throughout", 

351 "Enable Brotli compression", 

352 ], 

353 }, 

354 "security": { 

355 "authentication": [ 

356 "Use secure session management", 

357 "Implement CSRF protection", 

358 "Validate all user inputs", 

359 ], 

360 "templates": [ 

361 "Auto-escape by default", 

362 "Use safe filters when needed", 

363 "Sanitize user-generated content", 

364 ], 

365 }, 

366 } 

367 

368 

369# API Resources 

370 

371 

372async def get_route_definitions() -> dict[str, Any]: 

373 """Get route definitions from FastBlocks application. 

374 

375 Returns: 

376 Dict with route information 

377 """ 

378 try: 

379 # Try to get routes from ACB registry 

380 routes_adapter = depends.get("routes") 

381 if routes_adapter is None: 

382 return { 

383 "success": False, 

384 "error": "Routes adapter not available", 

385 } 

386 

387 # This would need actual route introspection 

388 # For now, return basic structure 

389 return { 

390 "name": "FastBlocks Route Definitions", 

391 "routes": [ 

392 { 

393 "path": "/", 

394 "methods": ["GET"], 

395 "handler": "index", 

396 "description": "Home page", 

397 }, 

398 ], 

399 "note": "Route definitions depend on application configuration", 

400 } 

401 

402 except Exception as e: 

403 logger.error(f"Error getting route definitions: {e}") 

404 return {"success": False, "error": str(e)} 

405 

406 

407async def get_htmx_patterns() -> dict[str, Any]: 

408 """Get HTMX integration patterns for FastBlocks. 

409 

410 Returns: 

411 Dict with HTMX pattern documentation 

412 """ 

413 return { 

414 "name": "FastBlocks HTMX Integration Patterns", 

415 "common_patterns": { 

416 "inline_editing": { 

417 "description": "Click to edit inline", 

418 "template_example": """<div hx-get="/edit/[[ item.id ]]" 

419 hx-trigger="click" 

420 hx-target="this" 

421 hx-swap="outerHTML"> 

422 [[ item.name ]] 

423</div>""", 

424 }, 

425 "infinite_scroll": { 

426 "description": "Load more on scroll", 

427 "template_example": """<div hx-get="/items?page=[[ page + 1 ]]" 

428 hx-trigger="revealed" 

429 hx-swap="afterend"> 

430</div>""", 

431 }, 

432 "active_search": { 

433 "description": "Search as you type", 

434 "template_example": """<input type="text" 

435 hx-get="/search" 

436 hx-trigger="keyup changed delay:500ms" 

437 hx-target="#results">""", 

438 }, 

439 "delete_confirmation": { 

440 "description": "Confirm before delete", 

441 "template_example": """<button hx-delete="/item/[[ item.id ]]" 

442 hx-confirm="Are you sure?" 

443 hx-target="closest .item" 

444 hx-swap="outerHTML swap:1s"> 

445 Delete 

446</button>""", 

447 }, 

448 }, 

449 "response_helpers": { 

450 "htmx_trigger": "Trigger client-side events", 

451 "htmx_redirect": "Client-side redirect", 

452 "htmx_refresh": "Refresh current page", 

453 "htmx_response": "Custom HTMX response", 

454 }, 

455 } 

456 

457 

458# Resource registration function 

459 

460 

461async def register_fastblocks_resources(server: Any) -> None: 

462 """Register all FastBlocks MCP resources with the server. 

463 

464 Args: 

465 server: MCP server instance from ACB 

466 """ 

467 try: 

468 from acb.mcp import register_resources # type: ignore[attr-defined] 

469 

470 # Define resource registry 

471 resources = { 

472 # Template resources 

473 "template_syntax": get_template_syntax_reference, 

474 "template_filters": get_available_filters, 

475 "component_catalog": get_htmy_component_catalog, 

476 # Configuration resources 

477 "adapter_schemas": get_adapter_schemas, 

478 "settings_docs": get_settings_documentation, 

479 "best_practices": get_best_practices, 

480 # API resources 

481 "route_definitions": get_route_definitions, 

482 "htmx_patterns": get_htmx_patterns, 

483 } 

484 

485 # Register resources with MCP server 

486 await register_resources(server, resources) # type: ignore[misc] 

487 

488 logger.info(f"Registered {len(resources)} FastBlocks MCP resources") 

489 

490 except Exception as e: 

491 logger.error(f"Failed to register MCP resources: {e}") 

492 raise