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
« 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."""
3import logging
4from typing import Any
6from acb.depends import depends
8logger = logging.getLogger(__name__)
11# Template Resources
14async def get_template_syntax_reference() -> dict[str, Any]:
15 """Get FastBlocks template syntax reference.
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 }
83async def get_available_filters() -> dict[str, Any]:
84 """Get list of available template filters.
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 }
190async def get_htmy_component_catalog() -> dict[str, Any]:
191 """Get catalog of available HTMY components.
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 }
204 components = await htmy_adapter.discover_components()
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 }
228 except Exception as e:
229 logger.error(f"Error getting component catalog: {e}")
230 return {"success": False, "error": str(e)}
233# Configuration Resources
236async def get_adapter_schemas() -> dict[str, Any]:
237 """Get configuration schemas for all adapters.
239 Returns:
240 Dict with adapter configuration schemas
241 """
242 try:
243 from .discovery import AdapterDiscoveryServer
245 discovery = AdapterDiscoveryServer()
246 adapters = await discovery.discover_adapters()
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 }
267 except Exception as e:
268 logger.error(f"Error getting adapter schemas: {e}")
269 return {"success": False, "error": str(e)}
272async def get_settings_documentation() -> dict[str, Any]:
273 """Get FastBlocks settings documentation.
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 }
317async def get_best_practices() -> dict[str, Any]:
318 """Get FastBlocks best practices guide.
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 }
369# API Resources
372async def get_route_definitions() -> dict[str, Any]:
373 """Get route definitions from FastBlocks application.
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 }
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 }
402 except Exception as e:
403 logger.error(f"Error getting route definitions: {e}")
404 return {"success": False, "error": str(e)}
407async def get_htmx_patterns() -> dict[str, Any]:
408 """Get HTMX integration patterns for FastBlocks.
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 }
458# Resource registration function
461async def register_fastblocks_resources(server: Any) -> None:
462 """Register all FastBlocks MCP resources with the server.
464 Args:
465 server: MCP server instance from ACB
466 """
467 try:
468 from acb.mcp import register_resources # type: ignore[attr-defined]
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 }
485 # Register resources with MCP server
486 await register_resources(server, resources) # type: ignore[misc]
488 logger.info(f"Registered {len(resources)} FastBlocks MCP resources")
490 except Exception as e:
491 logger.error(f"Failed to register MCP resources: {e}")
492 raise