@symbo.ls/mcp 1.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/.env.example +16 -0
- package/.env.railway +13 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/mcp.json +57 -0
- package/package.json +20 -0
- package/pyproject.toml +25 -0
- package/railway.toml +26 -0
- package/run.sh +17 -0
- package/symbols_mcp/__init__.py +1 -0
- package/symbols_mcp/server.py +1114 -0
- package/symbols_mcp/skills/ACCESSIBILITY.md +471 -0
- package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +70 -0
- package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +257 -0
- package/symbols_mcp/skills/BRAND_INDENTITY.md +69 -0
- package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +304 -0
- package/symbols_mcp/skills/CLAUDE.md +2158 -0
- package/symbols_mcp/skills/CLI_QUICK_START.md +205 -0
- package/symbols_mcp/skills/DESIGN_CRITIQUE.md +64 -0
- package/symbols_mcp/skills/DESIGN_DIRECTION.md +320 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +64 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +487 -0
- package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +136 -0
- package/symbols_mcp/skills/DESIGN_TO_CODE.md +64 -0
- package/symbols_mcp/skills/DESIGN_TREND.md +50 -0
- package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +236 -0
- package/symbols_mcp/skills/FIGMA_MATCHING.md +63 -0
- package/symbols_mcp/skills/GARY_TAN.md +80 -0
- package/symbols_mcp/skills/MARKETING_ASSETS.md +66 -0
- package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +614 -0
- package/symbols_mcp/skills/QUICKSTART.md +79 -0
- package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +1405 -0
- package/symbols_mcp/skills/THE_PRESENTATION.md +69 -0
- package/symbols_mcp/skills/UI_UX_PATTERNS.md +68 -0
- package/windsurf-mcp-config.json +18 -0
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symbols MCP Server — Exposes Symbols/DOMQL AI assistant capabilities
|
|
3
|
+
as tools, resources, and prompts for Cursor, Claude Code, and Windsurf.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
from mcp.server.fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Configuration
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
load_dotenv()
|
|
22
|
+
|
|
23
|
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
|
24
|
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
|
25
|
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
|
|
26
|
+
LLM_MODEL = os.getenv("LLM_MODEL", "openai/gpt-4.1-mini")
|
|
27
|
+
SKILLS_DIR = os.getenv("SYMBOLS_SKILLS_DIR", str(Path(__file__).resolve().parent / "skills"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fetch_remote_config() -> None:
|
|
31
|
+
"""Fetch Supabase credentials from the proxy server when not set locally."""
|
|
32
|
+
global SUPABASE_URL, SUPABASE_KEY
|
|
33
|
+
proxy_url = os.getenv("SYMBOLS_MCP_URL")
|
|
34
|
+
if not proxy_url or (SUPABASE_URL and SUPABASE_KEY):
|
|
35
|
+
return
|
|
36
|
+
try:
|
|
37
|
+
import httpx as _httpx
|
|
38
|
+
resp = _httpx.get(f"{proxy_url}/api/config", timeout=10.0)
|
|
39
|
+
if resp.status_code == 200:
|
|
40
|
+
data = resp.json()
|
|
41
|
+
SUPABASE_URL = data.get("supabase_url", "")
|
|
42
|
+
SUPABASE_KEY = data.get("supabase_key", "")
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_fetch_remote_config()
|
|
48
|
+
|
|
49
|
+
logging.basicConfig(level=logging.INFO)
|
|
50
|
+
logger = logging.getLogger("symbols-mcp")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_agent_instructions() -> str:
|
|
54
|
+
"""Load the upfront AI agent instructions from AGENT_INSTRUCTIONS.md."""
|
|
55
|
+
path = Path(SKILLS_DIR) / "AGENT_INSTRUCTIONS.md"
|
|
56
|
+
if path.exists():
|
|
57
|
+
return path.read_text(encoding="utf-8")
|
|
58
|
+
return (
|
|
59
|
+
"AI-powered assistant for the Symbols design-system framework. "
|
|
60
|
+
"Generates DOMQL v3 components, pages, and full projects; converts "
|
|
61
|
+
"React/Angular/Vue code to Symbols; searches Symbols documentation; "
|
|
62
|
+
"and provides comprehensive framework reference."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# MCP Server
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
mcp = FastMCP(
|
|
70
|
+
"Symbols AI Assistant",
|
|
71
|
+
instructions=_load_agent_instructions(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Helpers
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
SKILLS_PATH = Path(SKILLS_DIR)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _read_skill(filename: str) -> str:
|
|
81
|
+
"""Read a skill markdown file from the skills directory."""
|
|
82
|
+
path = SKILLS_PATH / filename
|
|
83
|
+
if path.exists():
|
|
84
|
+
return path.read_text(encoding="utf-8")
|
|
85
|
+
return f"Skill file '{filename}' not found at {path}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _call_openrouter(prompt: str, max_tokens: int = 4000) -> str:
|
|
89
|
+
"""Call OpenRouter API for AI generation via proxy or direct.
|
|
90
|
+
|
|
91
|
+
Retries up to 3 times with exponential backoff on transient
|
|
92
|
+
network errors (DNS failures, connection resets, timeouts).
|
|
93
|
+
"""
|
|
94
|
+
proxy_url = os.getenv("SYMBOLS_MCP_URL")
|
|
95
|
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
|
96
|
+
|
|
97
|
+
if not proxy_url and not api_key:
|
|
98
|
+
return "Error: Either SYMBOLS_MCP_URL or OPENROUTER_API_KEY must be set"
|
|
99
|
+
|
|
100
|
+
# Determine target host for diagnostics
|
|
101
|
+
if proxy_url:
|
|
102
|
+
from urllib.parse import urlparse
|
|
103
|
+
target_host = urlparse(proxy_url).netloc or proxy_url
|
|
104
|
+
else:
|
|
105
|
+
target_host = "openrouter.ai"
|
|
106
|
+
|
|
107
|
+
max_retries = 4
|
|
108
|
+
last_error: Exception | None = None
|
|
109
|
+
|
|
110
|
+
for attempt in range(max_retries):
|
|
111
|
+
if attempt > 0:
|
|
112
|
+
wait = 3 * attempt # 3s, 6s, 9s
|
|
113
|
+
logger.warning("Network error on attempt %d/%d, retrying in %ds: %s", attempt + 1, max_retries, wait, last_error)
|
|
114
|
+
await asyncio.sleep(wait)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
async with httpx.AsyncClient() as client:
|
|
118
|
+
if proxy_url:
|
|
119
|
+
response = await client.post(
|
|
120
|
+
f"{proxy_url}/api/chat",
|
|
121
|
+
headers={"Content-Type": "application/json"},
|
|
122
|
+
json={
|
|
123
|
+
"model": os.getenv("LLM_MODEL", "openai/gpt-4.1-mini"),
|
|
124
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
125
|
+
"max_tokens": max_tokens,
|
|
126
|
+
"temperature": 0.7,
|
|
127
|
+
},
|
|
128
|
+
timeout=60.0,
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
response = await client.post(
|
|
132
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
133
|
+
headers={
|
|
134
|
+
"Authorization": f"Bearer {api_key}",
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
"HTTP-Referer": "https://github.com/baronsilver/symbols-mcp-server",
|
|
137
|
+
},
|
|
138
|
+
json={
|
|
139
|
+
"model": os.getenv("LLM_MODEL", "openai/gpt-4.1-mini"),
|
|
140
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
141
|
+
"max_tokens": max_tokens,
|
|
142
|
+
"temperature": 0.7,
|
|
143
|
+
},
|
|
144
|
+
timeout=60.0,
|
|
145
|
+
)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
return response.json()["choices"][0]["message"]["content"]
|
|
148
|
+
|
|
149
|
+
except httpx.ConnectError as e:
|
|
150
|
+
last_error = e
|
|
151
|
+
continue # retry — includes DNS failures from proxy cold-starts
|
|
152
|
+
except (httpx.TimeoutException, httpx.RemoteProtocolError) as e:
|
|
153
|
+
last_error = e
|
|
154
|
+
continue
|
|
155
|
+
except httpx.HTTPStatusError as e:
|
|
156
|
+
if e.response.status_code >= 500:
|
|
157
|
+
last_error = e
|
|
158
|
+
continue
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
f"Error: Network request to '{target_host}' failed after {max_retries} attempts. "
|
|
163
|
+
f"Last error: {last_error}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _clean_code_response(text: str) -> str:
|
|
168
|
+
"""Strip markdown code fences from an AI response."""
|
|
169
|
+
cleaned = text.strip()
|
|
170
|
+
for prefix in ("```javascript", "```js", "```json", "```"):
|
|
171
|
+
if cleaned.startswith(prefix):
|
|
172
|
+
cleaned = cleaned[len(prefix) :]
|
|
173
|
+
break
|
|
174
|
+
if cleaned.endswith("```"):
|
|
175
|
+
cleaned = cleaned[:-3]
|
|
176
|
+
return cleaned.strip()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _build_symbols_system_context() -> str:
|
|
180
|
+
"""Build the core Symbols/DOMQL v3 knowledge context from skills files."""
|
|
181
|
+
parts = []
|
|
182
|
+
for fname in ("CLAUDE.md", "SYMBOLS_LOCAL_INSTRUCTIONS.md", "DESIGN_DIRECTION.md"):
|
|
183
|
+
content = _read_skill(fname)
|
|
184
|
+
if not content.startswith("Skill file"):
|
|
185
|
+
parts.append(content)
|
|
186
|
+
return "\n\n---\n\n".join(parts)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Cache the system context at module load
|
|
190
|
+
_SYMBOLS_CONTEXT: str | None = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _get_symbols_context() -> str:
|
|
194
|
+
global _SYMBOLS_CONTEXT
|
|
195
|
+
if _SYMBOLS_CONTEXT is None:
|
|
196
|
+
_SYMBOLS_CONTEXT = _build_symbols_system_context()
|
|
197
|
+
return _SYMBOLS_CONTEXT
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# TOOLS
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@mcp.tool()
|
|
206
|
+
def get_project_rules() -> str:
|
|
207
|
+
"""ALWAYS call this first before any generate_* tool.
|
|
208
|
+
|
|
209
|
+
Returns the mandatory Symbols/DOMQL v3 rules that MUST be followed.
|
|
210
|
+
Violations cause silent failures — black page, nothing renders.
|
|
211
|
+
|
|
212
|
+
Call this before: generate_project, generate_component, generate_page,
|
|
213
|
+
convert_to_symbols, or any code generation task.
|
|
214
|
+
"""
|
|
215
|
+
return _load_agent_instructions()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@mcp.tool()
|
|
219
|
+
async def generate_component(
|
|
220
|
+
description: str,
|
|
221
|
+
component_name: str = "GeneratedComponent",
|
|
222
|
+
interactive: bool = False,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Generate a Symbols/DOMQL v3 component from a natural-language description.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
description: What the component should do/look like (e.g. "a pricing card with 3 tiers").
|
|
228
|
+
component_name: PascalCase name for the component export.
|
|
229
|
+
interactive: If True, include event handlers and state management.
|
|
230
|
+
"""
|
|
231
|
+
ctx = _get_symbols_context()
|
|
232
|
+
|
|
233
|
+
interactive_note = ""
|
|
234
|
+
if interactive:
|
|
235
|
+
interactive_note = """
|
|
236
|
+
IMPORTANT — This component must be INTERACTIVE:
|
|
237
|
+
- Include realistic event handlers (onClick, onInput, onSubmit, etc.)
|
|
238
|
+
- Add state management where needed (state: { ... }, s.update({ ... }))
|
|
239
|
+
- Use scope: { ... } for local helper functions
|
|
240
|
+
- Make buttons, inputs, and toggles functional
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
prompt = f"""You are an expert Symbols/DOMQL v3 developer. Generate a production-ready component.
|
|
244
|
+
|
|
245
|
+
{ctx}
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
TASK: Create a component named `{component_name}` based on this description:
|
|
250
|
+
|
|
251
|
+
{description}
|
|
252
|
+
|
|
253
|
+
{interactive_note}
|
|
254
|
+
|
|
255
|
+
RULES:
|
|
256
|
+
1. Output ONLY the JavaScript code — no markdown, no explanations.
|
|
257
|
+
2. Use DOMQL v3 syntax exclusively (extends, childExtends, flattened props, onX events).
|
|
258
|
+
3. Use design-system tokens for spacing (padding: 'A', gap: 'B'), colors, and typography.
|
|
259
|
+
4. Components are plain objects with named exports: export const {component_name} = {{ ... }}
|
|
260
|
+
5. NO imports between project files. Reference child components by PascalCase key.
|
|
261
|
+
6. Keep folders flat — this is a single component file.
|
|
262
|
+
7. Follow the modern UI/UX direction: clarity, hierarchy, minimal cognitive load.
|
|
263
|
+
|
|
264
|
+
CRITICAL ICON RULES — ALWAYS FOLLOW:
|
|
265
|
+
8. NEVER use `Icon` component inside `Button` or `Flex+tag:button` — it will NOT render.
|
|
266
|
+
9. For icon buttons, use `extends: 'Flex', tag: 'button'` with a `Svg` child (key must be `Svg`):
|
|
267
|
+
MyBtn: {{ extends: 'Flex', tag: 'button', flexAlign: 'center center', cursor: 'pointer',
|
|
268
|
+
Svg: {{ viewBox: '0 0 24 24', width: '22', height: '22', color: 'flame',
|
|
269
|
+
html: '<path d="..." fill="currentColor"/>' }} }}
|
|
270
|
+
10. The `html` prop ONLY works on the `Svg` atom — NOT on Flex/Box/Button.
|
|
271
|
+
11. For standalone SVG icons (not in buttons), use key name `Svg` directly.
|
|
272
|
+
12. Use `flexAlign` (not `align`) for alignItems+justifyContent shorthand on Flex.
|
|
273
|
+
13. Functions called via `el.call('fnName', arg)` receive the element as `this`, NOT as first arg.
|
|
274
|
+
Inside functions: use `this.node` for DOM access. NEVER pass `el` as an argument to `el.call()`.
|
|
275
|
+
14. In `onRender`, guard against double-init: `if (el.__initialized) return; el.__initialized = true`.
|
|
276
|
+
|
|
277
|
+
OUTPUT:
|
|
278
|
+
"""
|
|
279
|
+
response = await _call_openrouter(prompt)
|
|
280
|
+
return _clean_code_response(response)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@mcp.tool()
|
|
284
|
+
async def generate_page(
|
|
285
|
+
description: str,
|
|
286
|
+
page_name: str = "main",
|
|
287
|
+
route: str = "/",
|
|
288
|
+
) -> str:
|
|
289
|
+
"""Generate a Symbols/DOMQL v3 page with routing support.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
description: What the page should contain (e.g. "a dashboard with metrics cards and a chart").
|
|
293
|
+
page_name: camelCase name for the page export.
|
|
294
|
+
route: The URL route for this page (e.g. "/dashboard").
|
|
295
|
+
"""
|
|
296
|
+
ctx = _get_symbols_context()
|
|
297
|
+
prompt = f"""You are an expert Symbols/DOMQL v3 developer. Generate a production-ready page.
|
|
298
|
+
|
|
299
|
+
{ctx}
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
TASK: Create a page named `{page_name}` (route: {route}) based on this description:
|
|
304
|
+
|
|
305
|
+
{description}
|
|
306
|
+
|
|
307
|
+
RULES:
|
|
308
|
+
1. Output ONLY the JavaScript code — no markdown, no explanations.
|
|
309
|
+
2. Pages extend from 'Page': export const {page_name} = {{ extends: 'Page', ... }}
|
|
310
|
+
3. Use DOMQL v3 syntax exclusively.
|
|
311
|
+
4. Use design-system tokens for all spacing, colors, typography.
|
|
312
|
+
5. Reference child components by PascalCase key name — no imports.
|
|
313
|
+
6. Include onRender/onInit for data loading if the page needs dynamic data.
|
|
314
|
+
7. Follow modern UI/UX: clear hierarchy, confident typography, balanced composition.
|
|
315
|
+
8. Pages use dash-case filenames but camelCase exports.
|
|
316
|
+
|
|
317
|
+
CRITICAL RULES — ALWAYS FOLLOW:
|
|
318
|
+
9. NEVER use `Icon` inside `Button` or `Flex+tag:button` — use `Svg` atom with `html` prop instead.
|
|
319
|
+
IconBtn: {{ extends: 'Flex', tag: 'button', flexAlign: 'center center', cursor: 'pointer',
|
|
320
|
+
Svg: {{ viewBox: '0 0 24 24', width: '22', height: '22', color: 'primary',
|
|
321
|
+
html: '<path d="..." fill="currentColor"/>' }} }}
|
|
322
|
+
10. `html` prop ONLY works on `Svg` atom — NOT on Flex/Box/Button.
|
|
323
|
+
11. Use `flexAlign` (not `align`) for combined alignItems+justifyContent on Flex.
|
|
324
|
+
12. `el.call('fn', arg)` passes element as `this` inside fn — NEVER pass `el` as argument.
|
|
325
|
+
13. Guard `onRender` against double-init: `if (el.__initialized) return; el.__initialized = true`.
|
|
326
|
+
|
|
327
|
+
OUTPUT:
|
|
328
|
+
"""
|
|
329
|
+
response = await _call_openrouter(prompt)
|
|
330
|
+
return _clean_code_response(response)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.tool()
|
|
334
|
+
async def generate_project(
|
|
335
|
+
description: str,
|
|
336
|
+
project_name: str = "my-symbols-app",
|
|
337
|
+
) -> str:
|
|
338
|
+
"""Generate a complete multi-file Symbols/DOMQL v3 project structure.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
description: What the application should be (e.g. "a restaurant website with menu, about, and contact pages").
|
|
342
|
+
project_name: Name for the project.
|
|
343
|
+
"""
|
|
344
|
+
ctx = _get_symbols_context()
|
|
345
|
+
local_instructions = _read_skill("SYMBOLS_LOCAL_INSTRUCTIONS.md")
|
|
346
|
+
|
|
347
|
+
prompt = f"""You are an expert Symbols/DOMQL v3 architect. Generate a COMPLETE project.
|
|
348
|
+
|
|
349
|
+
{ctx}
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
PROJECT STRUCTURE REFERENCE:
|
|
354
|
+
{local_instructions}
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
TASK: Create a complete Symbols project called "{project_name}" based on:
|
|
359
|
+
|
|
360
|
+
{description}
|
|
361
|
+
|
|
362
|
+
OUTPUT FORMAT — Return a JSON object with this exact structure:
|
|
363
|
+
{{
|
|
364
|
+
"type": "project_structure",
|
|
365
|
+
"title": "Project title",
|
|
366
|
+
"description": "Brief description",
|
|
367
|
+
"files": [
|
|
368
|
+
{{
|
|
369
|
+
"path": "smbls/index.js",
|
|
370
|
+
"language": "javascript",
|
|
371
|
+
"code": "// file contents here"
|
|
372
|
+
}}
|
|
373
|
+
]
|
|
374
|
+
}}
|
|
375
|
+
|
|
376
|
+
MANDATORY FILE TEMPLATES — every generated project MUST follow these exactly:
|
|
377
|
+
|
|
378
|
+
smbls/components/index.js — MUST use `export *` NOT `export * as`:
|
|
379
|
+
export * from './Navbar.js'
|
|
380
|
+
export * from './Card.js'
|
|
381
|
+
// one line per component — NEVER: export * as Navbar from './Navbar.js'
|
|
382
|
+
|
|
383
|
+
smbls/pages/index.js — the ONLY file where imports are allowed:
|
|
384
|
+
import {{ main }} from './main.js'
|
|
385
|
+
export default {{ '/': main }}
|
|
386
|
+
|
|
387
|
+
smbls/pages/main.js — pages MUST extend 'Page':
|
|
388
|
+
export const main = {{ extends: 'Page', ... }}
|
|
389
|
+
// NEVER: extends: 'Flex' or extends: 'Box' for a page
|
|
390
|
+
|
|
391
|
+
smbls/functions/index.js:
|
|
392
|
+
export * from './myFunction.js'
|
|
393
|
+
|
|
394
|
+
smbls/functions/myFunction.js — functions use named function expressions:
|
|
395
|
+
export const myFunction = function myFunction(arg) {{
|
|
396
|
+
const node = this.node // 'this' is the DOMQL element, NOT a parameter
|
|
397
|
+
}}
|
|
398
|
+
|
|
399
|
+
smbls/state.js — inline initial state, NO imports:
|
|
400
|
+
export default {{ activePage: 'home', user: {{}}, items: [] }}
|
|
401
|
+
|
|
402
|
+
smbls/index.js — root registry:
|
|
403
|
+
export {{ default as state }} from './state.js'
|
|
404
|
+
export {{ default as dependencies }} from './dependencies.js'
|
|
405
|
+
export * as components from './components/index.js'
|
|
406
|
+
export {{ default as pages }} from './pages/index.js'
|
|
407
|
+
export * as functions from './functions/index.js'
|
|
408
|
+
export * as methods from './methods/index.js'
|
|
409
|
+
export {{ default as designSystem }} from './designSystem/index.js'
|
|
410
|
+
|
|
411
|
+
MULTI-VIEW NAVIGATION — use DOM IDs + switchView function, NOT reactive display bindings:
|
|
412
|
+
// In main page — assign id to each view:
|
|
413
|
+
HomeView: {{ id: 'view-home', extends: 'Flex', flexDirection: 'column' }},
|
|
414
|
+
AboutView: {{ id: 'view-about', extends: 'Flex', flexDirection: 'column', display: 'none' }},
|
|
415
|
+
// In Navbar onClick:
|
|
416
|
+
onClick: (e, el) => {{ el.call('switchView', 'about') }}
|
|
417
|
+
// In functions/switchView.js:
|
|
418
|
+
export const switchView = function switchView(view) {{
|
|
419
|
+
['home', 'about'].forEach(function(v) {{
|
|
420
|
+
const el = document.getElementById('view-' + v)
|
|
421
|
+
if (el) el.style.display = v === view ? 'flex' : 'none'
|
|
422
|
+
}})
|
|
423
|
+
}}
|
|
424
|
+
|
|
425
|
+
RULES:
|
|
426
|
+
1. Include ALL required files: smbls/index.js, smbls/state.js, smbls/dependencies.js, smbls/pages/index.js, smbls/components/index.js, smbls/functions/index.js, smbls/designSystem/index.js
|
|
427
|
+
2. Generate meaningful component, page, and function files for the described app.
|
|
428
|
+
3. Use DOMQL v3 syntax exclusively — NO React/Vue/Angular syntax.
|
|
429
|
+
4. Use design-system tokens for spacing/colors — NOT hardcoded pixel values.
|
|
430
|
+
5. All folders are FLAT — no subfolders within components/, pages/, functions/, etc.
|
|
431
|
+
6. Components: named exports (`export const X = {{}}`). DesignSystem: default exports.
|
|
432
|
+
7. NO imports between component/function/page files — reference components by PascalCase key in tree.
|
|
433
|
+
8. Output ONLY the JSON — no markdown fences, no explanations.
|
|
434
|
+
|
|
435
|
+
CRITICAL RULES — violations cause silent failures (black page, nothing renders):
|
|
436
|
+
9. `components/index.js`: ALWAYS `export * from './X.js'` — NEVER `export * as X from './X.js'`
|
|
437
|
+
10. Pages: ALWAYS `extends: 'Page'` — NEVER `extends: 'Flex'` or `extends: 'Box'`
|
|
438
|
+
11. NEVER use `Icon` inside `Button` or `Flex+tag:button` — use `Svg` atom with `html` prop:
|
|
439
|
+
Btn: {{ extends: 'Flex', tag: 'button', flexAlign: 'center center', cursor: 'pointer',
|
|
440
|
+
Svg: {{ viewBox: '0 0 24 24', width: '22', height: '22',
|
|
441
|
+
html: '<path d="..." fill="currentColor"/>' }} }}
|
|
442
|
+
12. `html` prop ONLY works on `Svg` atom — NOT on Flex/Box/Button.
|
|
443
|
+
13. `flexAlign` (not `align`) for alignItems+justifyContent shorthand on Flex.
|
|
444
|
+
14. `el.call('fn', arg)` — element is `this` inside fn — NEVER pass `el` as argument.
|
|
445
|
+
15. Guard `onRender`: `if (el.__initialized) return; el.__initialized = true`
|
|
446
|
+
16. State updates: `s.update({{ key: val }})` — NEVER mutate `s.key = val` directly.
|
|
447
|
+
17. `childExtends` MUST be a string name — NEVER an inline object. Inline objects dump all prop values as visible text on every child:
|
|
448
|
+
WRONG: childExtends: {{ tag: 'button', color: 'white', border: '2px solid transparent' }}
|
|
449
|
+
CORRECT: childExtends: 'NavLink' (define NavLink as a named component in components/)
|
|
450
|
+
18. Color opacity: NEVER use `color: 'white .7'` — it renders as raw text. Define named tokens:
|
|
451
|
+
COLOR.js: {{ whiteMuted: 'rgba(255,255,255,0.7)', whiteSubtle: 'rgba(255,255,255,0.6)' }}
|
|
452
|
+
Then use: `color: 'whiteMuted'`
|
|
453
|
+
18. Border shorthand: NEVER use `border: '2px solid transparent'` — it renders as raw text.
|
|
454
|
+
Always split: `borderWidth: '2px', borderStyle: 'solid', borderColor: 'transparent'`
|
|
455
|
+
Only `border: 'none'` is safe as a shorthand.
|
|
456
|
+
|
|
457
|
+
OUTPUT:
|
|
458
|
+
"""
|
|
459
|
+
response = await _call_openrouter(prompt, max_tokens=16000)
|
|
460
|
+
cleaned = _clean_code_response(response)
|
|
461
|
+
|
|
462
|
+
# Validate JSON
|
|
463
|
+
try:
|
|
464
|
+
parsed = json.loads(cleaned)
|
|
465
|
+
return json.dumps(parsed, indent=2)
|
|
466
|
+
except json.JSONDecodeError:
|
|
467
|
+
return cleaned
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@mcp.tool()
|
|
471
|
+
async def convert_to_symbols(
|
|
472
|
+
code: str,
|
|
473
|
+
source_framework: str = "auto",
|
|
474
|
+
) -> str:
|
|
475
|
+
"""Convert React, Angular, Vue, or HTML code to Symbols/DOMQL v3 format.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
code: The source code to convert (React JSX, Angular template, Vue SFC, or HTML).
|
|
479
|
+
source_framework: Source framework — "auto", "react", "angular", "vue", or "html".
|
|
480
|
+
"""
|
|
481
|
+
migration_guide = _read_skill("MIGRATE_TO_SYMBOLS.md")
|
|
482
|
+
v3_migration = _read_skill("DOMQL_v2-v3_MIGRATION.md")
|
|
483
|
+
ctx = _get_symbols_context()
|
|
484
|
+
|
|
485
|
+
prompt = f"""You are an expert migration assistant converting code to Symbols/DOMQL v3.
|
|
486
|
+
|
|
487
|
+
{ctx}
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
MIGRATION REFERENCE:
|
|
492
|
+
{migration_guide}
|
|
493
|
+
|
|
494
|
+
V2→V3 CHANGES:
|
|
495
|
+
{v3_migration}
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
TASK: Convert this {source_framework} code to Symbols/DOMQL v3 format:
|
|
500
|
+
|
|
501
|
+
```
|
|
502
|
+
{code}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
RULES:
|
|
506
|
+
1. Output ONLY the converted Symbols/DOMQL v3 code — no markdown, no explanations.
|
|
507
|
+
2. Use v3 syntax ONLY: extends (not extend), childExtends (not childExtend), flattened props (no props: wrapper), onX events (no on: wrapper).
|
|
508
|
+
3. Replace all framework-specific patterns (useState, useEffect, v-if, *ngFor, etc.) with Symbols equivalents.
|
|
509
|
+
4. Use design-system tokens for spacing and colors where possible.
|
|
510
|
+
5. NO imports between project files — reference components by PascalCase key.
|
|
511
|
+
6. Components are plain objects with named exports.
|
|
512
|
+
7. Extract styles into flattened props with design tokens.
|
|
513
|
+
|
|
514
|
+
OUTPUT:
|
|
515
|
+
"""
|
|
516
|
+
response = await _call_openrouter(prompt, max_tokens=12000)
|
|
517
|
+
return _clean_code_response(response)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@mcp.tool()
|
|
521
|
+
async def search_symbols_docs(
|
|
522
|
+
query: str,
|
|
523
|
+
max_results: int = 3,
|
|
524
|
+
) -> str:
|
|
525
|
+
"""Search the Symbols documentation knowledge base for relevant information.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
query: Natural language search query about Symbols/DOMQL.
|
|
529
|
+
max_results: Maximum number of results to return (1-5).
|
|
530
|
+
"""
|
|
531
|
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
|
532
|
+
# Fall back to local skills search if no Supabase
|
|
533
|
+
keywords = [w for w in re.split(r"\s+", query.lower()) if len(w) > 2]
|
|
534
|
+
if not keywords:
|
|
535
|
+
keywords = [query.lower()]
|
|
536
|
+
results = []
|
|
537
|
+
for fname in SKILLS_PATH.glob("*.md"):
|
|
538
|
+
content = fname.read_text(encoding="utf-8")
|
|
539
|
+
content_lower = content.lower()
|
|
540
|
+
if not any(kw in content_lower for kw in keywords):
|
|
541
|
+
continue
|
|
542
|
+
# Find the first line that contains any keyword
|
|
543
|
+
lines = content.split("\n")
|
|
544
|
+
for i, line in enumerate(lines):
|
|
545
|
+
line_lower = line.lower()
|
|
546
|
+
if any(kw in line_lower for kw in keywords):
|
|
547
|
+
start = max(0, i - 2)
|
|
548
|
+
end = min(len(lines), i + 20)
|
|
549
|
+
snippet = "\n".join(lines[start:end])
|
|
550
|
+
results.append({"file": fname.name, "snippet": snippet})
|
|
551
|
+
break
|
|
552
|
+
if len(results) >= max_results:
|
|
553
|
+
break
|
|
554
|
+
|
|
555
|
+
if results:
|
|
556
|
+
return json.dumps(results, indent=2)
|
|
557
|
+
return f"No results found for '{query}'. Try a different search term."
|
|
558
|
+
|
|
559
|
+
# Always run local keyword search
|
|
560
|
+
keywords = [w for w in re.split(r"\s+", query.lower()) if len(w) > 2]
|
|
561
|
+
if not keywords:
|
|
562
|
+
keywords = [query.lower()]
|
|
563
|
+
local_results = []
|
|
564
|
+
for fname in SKILLS_PATH.glob("*.md"):
|
|
565
|
+
content = fname.read_text(encoding="utf-8")
|
|
566
|
+
content_lower = content.lower()
|
|
567
|
+
if not any(kw in content_lower for kw in keywords):
|
|
568
|
+
continue
|
|
569
|
+
lines = content.split("\n")
|
|
570
|
+
for i, line in enumerate(lines):
|
|
571
|
+
line_lower = line.lower()
|
|
572
|
+
if any(kw in line_lower for kw in keywords):
|
|
573
|
+
start = max(0, i - 2)
|
|
574
|
+
end = min(len(lines), i + 20)
|
|
575
|
+
snippet = "\n".join(lines[start:end])
|
|
576
|
+
local_results.append({"file": fname.name, "snippet": snippet})
|
|
577
|
+
break
|
|
578
|
+
if len(local_results) >= max_results:
|
|
579
|
+
break
|
|
580
|
+
|
|
581
|
+
# Also query Supabase vector search if available
|
|
582
|
+
supabase_results = []
|
|
583
|
+
try:
|
|
584
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
585
|
+
resp = await client.post(
|
|
586
|
+
f"{SUPABASE_URL}/rest/v1/rpc/match_documents",
|
|
587
|
+
headers={
|
|
588
|
+
"apikey": SUPABASE_KEY,
|
|
589
|
+
"Authorization": f"Bearer {SUPABASE_KEY}",
|
|
590
|
+
"Content-Type": "application/json",
|
|
591
|
+
},
|
|
592
|
+
json={"query_text": query, "match_count": max_results},
|
|
593
|
+
)
|
|
594
|
+
if resp.status_code == 200:
|
|
595
|
+
for doc in resp.json()[:max_results]:
|
|
596
|
+
supabase_results.append({
|
|
597
|
+
"title": doc.get("title", "Untitled"),
|
|
598
|
+
"content": doc.get("content", "")[:1000],
|
|
599
|
+
"similarity": doc.get("similarity", 0),
|
|
600
|
+
})
|
|
601
|
+
else:
|
|
602
|
+
logger.warning(f"Supabase search returned status {resp.status_code}.")
|
|
603
|
+
except Exception as e:
|
|
604
|
+
logger.warning(f"Supabase search failed: {e}")
|
|
605
|
+
|
|
606
|
+
results = supabase_results + local_results
|
|
607
|
+
if results:
|
|
608
|
+
return json.dumps(results[:max_results], indent=2)
|
|
609
|
+
return f"No results found for '{query}'. Try a different search term."
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@mcp.tool()
|
|
613
|
+
async def explain_symbols_concept(concept: str) -> str:
|
|
614
|
+
"""Explain a Symbols/DOMQL concept with examples (state, routing, events, design tokens, etc.).
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
concept: The concept to explain (e.g. "state management", "routing", "design tokens", "events", "children pattern").
|
|
618
|
+
"""
|
|
619
|
+
ctx = _get_symbols_context()
|
|
620
|
+
prompt = f"""You are an expert Symbols/DOMQL v3 instructor.
|
|
621
|
+
|
|
622
|
+
{ctx}
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
TASK: Explain the concept "{concept}" in Symbols/DOMQL v3.
|
|
627
|
+
|
|
628
|
+
RULES:
|
|
629
|
+
1. Give a clear, concise explanation (2-3 paragraphs max).
|
|
630
|
+
2. Include 1-2 practical code examples using DOMQL v3 syntax.
|
|
631
|
+
3. Highlight common mistakes and how to avoid them.
|
|
632
|
+
4. Reference relevant design-system tokens or patterns where applicable.
|
|
633
|
+
5. Do NOT use React/Vue/Angular — only Symbols/DOMQL v3.
|
|
634
|
+
|
|
635
|
+
OUTPUT:
|
|
636
|
+
"""
|
|
637
|
+
return await _call_openrouter(prompt, max_tokens=4000)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@mcp.tool()
|
|
641
|
+
async def review_symbols_code(code: str) -> str:
|
|
642
|
+
"""Review Symbols/DOMQL code for correctness, best practices, and v3 compliance.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
code: The Symbols/DOMQL code to review.
|
|
646
|
+
"""
|
|
647
|
+
ctx = _get_symbols_context()
|
|
648
|
+
prompt = f"""You are a strict Symbols/DOMQL v3 code reviewer.
|
|
649
|
+
|
|
650
|
+
{ctx}
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
TASK: Review this Symbols/DOMQL code for correctness and best practices:
|
|
655
|
+
|
|
656
|
+
```javascript
|
|
657
|
+
{code}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
CHECK FOR:
|
|
661
|
+
1. v2 syntax violations: extend (should be extends), childExtend (should be childExtends), props: {{ }} wrapper, on: {{ }} wrapper
|
|
662
|
+
2. Forbidden imports between project files
|
|
663
|
+
3. Function-based components (must be plain objects)
|
|
664
|
+
4. Subfolder usage (must be flat)
|
|
665
|
+
5. Hardcoded pixel values instead of design tokens
|
|
666
|
+
6. Incorrect event handler signatures
|
|
667
|
+
7. Missing or incorrect extends declarations
|
|
668
|
+
8. Default exports for components (should use named exports)
|
|
669
|
+
|
|
670
|
+
OUTPUT FORMAT:
|
|
671
|
+
- List issues found with line references
|
|
672
|
+
- Provide corrected code for each issue
|
|
673
|
+
- Give an overall score (1-10) for v3 compliance
|
|
674
|
+
- Suggest improvements for better Symbols patterns
|
|
675
|
+
|
|
676
|
+
OUTPUT:
|
|
677
|
+
"""
|
|
678
|
+
return await _call_openrouter(prompt, max_tokens=6000)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@mcp.tool()
|
|
682
|
+
async def create_design_system(
|
|
683
|
+
description: str,
|
|
684
|
+
include_theme: bool = True,
|
|
685
|
+
include_icons: bool = True,
|
|
686
|
+
) -> str:
|
|
687
|
+
"""Generate Symbols design system files (colors, spacing, typography, theme, icons).
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
description: Description of the design direction (e.g. "dark modern SaaS dashboard", "light minimal e-commerce").
|
|
691
|
+
include_theme: Whether to include theme definitions.
|
|
692
|
+
include_icons: Whether to include a basic icon set.
|
|
693
|
+
"""
|
|
694
|
+
ctx = _get_symbols_context()
|
|
695
|
+
design_direction = _read_skill("DESIGN_DIRECTION.md")
|
|
696
|
+
|
|
697
|
+
prompt = f"""You are an expert Symbols design-system architect.
|
|
698
|
+
|
|
699
|
+
{ctx}
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
DESIGN DIRECTION:
|
|
704
|
+
{design_direction}
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
TASK: Create a complete design system for: "{description}"
|
|
709
|
+
|
|
710
|
+
Generate the following files as a JSON object:
|
|
711
|
+
{{
|
|
712
|
+
"files": [
|
|
713
|
+
{{ "path": "designSystem/color.js", "code": "export default {{ ... }}" }},
|
|
714
|
+
{{ "path": "designSystem/spacing.js", "code": "export default {{ ... }}" }},
|
|
715
|
+
{{ "path": "designSystem/typography.js", "code": "export default {{ ... }}" }},
|
|
716
|
+
{('{{ "path": "designSystem/theme.js", "code": "..." }},' if include_theme else "")}
|
|
717
|
+
{('{{ "path": "designSystem/icons.js", "code": "..." }},' if include_icons else "")}
|
|
718
|
+
{{ "path": "designSystem/index.js", "code": "..." }}
|
|
719
|
+
]
|
|
720
|
+
}}
|
|
721
|
+
|
|
722
|
+
RULES:
|
|
723
|
+
1. Colors: Define a cohesive palette with semantic names. Support dark/light modes using array format.
|
|
724
|
+
2. Spacing: Use base + ratio system (default base: 16, ratio: 1.618).
|
|
725
|
+
3. Typography: Use base + ratio system (default base: 16, ratio: 1.25).
|
|
726
|
+
4. Theme: Define component themes (button, field, document, transparent) with @dark/@light variants.
|
|
727
|
+
5. Icons: Use inline SVG strings with camelCase keys and currentColor for fill/stroke.
|
|
728
|
+
6. Index: Import and re-export all design system modules.
|
|
729
|
+
7. All files use default exports.
|
|
730
|
+
8. Output ONLY the JSON — no markdown, no explanations.
|
|
731
|
+
|
|
732
|
+
OUTPUT:
|
|
733
|
+
"""
|
|
734
|
+
response = await _call_openrouter(prompt, max_tokens=10000)
|
|
735
|
+
cleaned = _clean_code_response(response)
|
|
736
|
+
try:
|
|
737
|
+
parsed = json.loads(cleaned)
|
|
738
|
+
return json.dumps(parsed, indent=2)
|
|
739
|
+
except json.JSONDecodeError:
|
|
740
|
+
return cleaned
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# ---------------------------------------------------------------------------
|
|
744
|
+
# RESOURCES — Expose skills documentation as browsable resources
|
|
745
|
+
# ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@mcp.resource("symbols://skills/domql-v3-reference")
|
|
749
|
+
def get_domql_v3_reference() -> str:
|
|
750
|
+
"""Complete DOMQL v3 syntax reference and rules."""
|
|
751
|
+
return _read_skill("CLAUDE.md")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@mcp.resource("symbols://skills/project-structure")
|
|
755
|
+
def get_project_structure() -> str:
|
|
756
|
+
"""Symbols project folder structure and file conventions."""
|
|
757
|
+
return _read_skill("SYMBOLS_LOCAL_INSTRUCTIONS.md")
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
@mcp.resource("symbols://skills/design-direction")
|
|
761
|
+
def get_design_direction() -> str:
|
|
762
|
+
"""Modern UI/UX design direction for generating Symbols interfaces."""
|
|
763
|
+
return _read_skill("DESIGN_DIRECTION.md")
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@mcp.resource("symbols://skills/migration-guide")
|
|
767
|
+
def get_migration_guide() -> str:
|
|
768
|
+
"""Guide for migrating React/Angular/Vue apps to Symbols/DOMQL v3."""
|
|
769
|
+
return _read_skill("MIGRATE_TO_SYMBOLS.md")
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
@mcp.resource("symbols://skills/v2-to-v3-migration")
|
|
773
|
+
def get_v2_v3_migration() -> str:
|
|
774
|
+
"""DOMQL v2 to v3 migration changes and examples."""
|
|
775
|
+
return _read_skill("DOMQL_v2-v3_MIGRATION.md")
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
@mcp.resource("symbols://skills/quickstart")
|
|
779
|
+
def get_quickstart() -> str:
|
|
780
|
+
"""Symbols CLI setup and usage quickstart guide."""
|
|
781
|
+
return _read_skill("QUICKSTART.md")
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@mcp.resource("symbols://reference/spacing-tokens")
|
|
785
|
+
def get_spacing_tokens() -> str:
|
|
786
|
+
"""Spacing token reference for the Symbols design system."""
|
|
787
|
+
return """# Symbols Spacing Tokens
|
|
788
|
+
|
|
789
|
+
Ratio-based system (base 16px, ratio 1.618 golden ratio):
|
|
790
|
+
|
|
791
|
+
| Token | ~px | Token | ~px | Token | ~px |
|
|
792
|
+
|-------|------|-------|------|-------|------|
|
|
793
|
+
| X | 3 | A | 16 | D | 67 |
|
|
794
|
+
| Y | 6 | A1 | 20 | E | 109 |
|
|
795
|
+
| Z | 10 | A2 | 22 | F | 177 |
|
|
796
|
+
| Z1 | 12 | B | 26 | | |
|
|
797
|
+
| Z2 | 14 | B1 | 32 | | |
|
|
798
|
+
| | | B2 | 36 | | |
|
|
799
|
+
| | | C | 42 | | |
|
|
800
|
+
| | | C1 | 52 | | |
|
|
801
|
+
| | | C2 | 55 | | |
|
|
802
|
+
|
|
803
|
+
Usage: padding: 'A B', gap: 'C', borderRadius: 'Z', fontSize: 'B1'
|
|
804
|
+
Tokens work with padding, margin, gap, width, height, borderRadius, position, and any spacing property.
|
|
805
|
+
Negative values: margin: '-Y1 -Z2 - auto'
|
|
806
|
+
Math: padding: 'A+V2'
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
@mcp.resource("symbols://reference/atom-components")
|
|
811
|
+
def get_atom_components() -> str:
|
|
812
|
+
"""Built-in primitive atom components in Symbols."""
|
|
813
|
+
return """# Symbols Atom Components (Primitives)
|
|
814
|
+
|
|
815
|
+
| Atom | HTML Tag | Description |
|
|
816
|
+
|------------|------------|-------------------------------|
|
|
817
|
+
| Text | <span> | Text content |
|
|
818
|
+
| Box | <div> | Generic container |
|
|
819
|
+
| Flex | <div> | Flexbox container |
|
|
820
|
+
| Grid | <div> | CSS Grid container |
|
|
821
|
+
| Link | <a> | Anchor with built-in router |
|
|
822
|
+
| Input | <input> | Form input |
|
|
823
|
+
| Radio | <input> | Radio button |
|
|
824
|
+
| Checkbox | <input> | Checkbox |
|
|
825
|
+
| Svg | <svg> | SVG container |
|
|
826
|
+
| Icon | <svg> | Icon from icon sprite |
|
|
827
|
+
| IconText | <div> | Icon + text combination |
|
|
828
|
+
| Button | <button> | Button with icon/text support |
|
|
829
|
+
| Img | <img> | Image element |
|
|
830
|
+
| Iframe | <iframe> | Embedded frame |
|
|
831
|
+
| Video | <video> | Video element |
|
|
832
|
+
|
|
833
|
+
Usage examples:
|
|
834
|
+
{ Box: { padding: 'A', background: 'surface' } }
|
|
835
|
+
{ Flex: { flow: 'y', gap: 'B', align: 'center center' } }
|
|
836
|
+
{ Grid: { columns: 'repeat(3, 1fr)', gap: 'A' } }
|
|
837
|
+
{ Link: { text: 'Click here', href: '/dashboard' } }
|
|
838
|
+
{ Button: { text: 'Submit', theme: 'primary', icon: 'check' } }
|
|
839
|
+
{ Icon: { name: 'chevronLeft' } }
|
|
840
|
+
{ Img: { src: 'photo.png', boxSize: 'D D' } }
|
|
841
|
+
"""
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
@mcp.resource("symbols://reference/event-handlers")
|
|
845
|
+
def get_event_handlers() -> str:
|
|
846
|
+
"""Event handler reference for Symbols/DOMQL v3."""
|
|
847
|
+
return """# Symbols Event Handlers (v3)
|
|
848
|
+
|
|
849
|
+
## Lifecycle Events
|
|
850
|
+
onInit: (el, state) => {} // Once on creation
|
|
851
|
+
onRender: (el, state) => {} // On each render (return fn for cleanup)
|
|
852
|
+
onUpdate: (el, state) => {} // On props/state change
|
|
853
|
+
onStateUpdate: (changes, el, state, context) => {}
|
|
854
|
+
|
|
855
|
+
## DOM Events
|
|
856
|
+
onClick: (event, el, state) => {}
|
|
857
|
+
onInput: (event, el, state) => {}
|
|
858
|
+
onKeydown: (event, el, state) => {}
|
|
859
|
+
onDblclick: (event, el, state) => {}
|
|
860
|
+
onMouseover: (event, el, state) => {}
|
|
861
|
+
onWheel: (event, el, state) => {}
|
|
862
|
+
onSubmit: (event, el, state) => {}
|
|
863
|
+
onLoad: (event, el, state) => {}
|
|
864
|
+
|
|
865
|
+
## Calling Functions
|
|
866
|
+
onClick: (e, el) => el.call('functionName', args) // Global function
|
|
867
|
+
onClick: (e, el) => el.scope.localFn(el, s) // Scope function
|
|
868
|
+
onClick: (e, el) => el.methodName() // Element method
|
|
869
|
+
|
|
870
|
+
## State Updates
|
|
871
|
+
onClick: (e, el, s) => s.update({ count: s.count + 1 })
|
|
872
|
+
onClick: (e, el, s) => s.toggle('isActive')
|
|
873
|
+
onClick: (e, el, s) => s.root.update({ modal: '/add-item' })
|
|
874
|
+
|
|
875
|
+
## Navigation
|
|
876
|
+
onClick: (e, el) => el.router('/dashboard', el.getRoot())
|
|
877
|
+
|
|
878
|
+
## Cleanup Pattern
|
|
879
|
+
onRender: (el, s) => {
|
|
880
|
+
const interval = setInterval(() => { /* ... */ }, 1000)
|
|
881
|
+
return () => clearInterval(interval) // Called on element removal
|
|
882
|
+
}
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
# ---------------------------------------------------------------------------
|
|
887
|
+
# PROMPTS — Reusable prompt templates for common tasks
|
|
888
|
+
# ---------------------------------------------------------------------------
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@mcp.prompt()
|
|
892
|
+
def symbols_component_prompt(description: str, component_name: str = "MyComponent") -> str:
|
|
893
|
+
"""Prompt template for generating a Symbols/DOMQL v3 component."""
|
|
894
|
+
return f"""Generate a Symbols/DOMQL v3 component with these requirements:
|
|
895
|
+
|
|
896
|
+
Component Name: {component_name}
|
|
897
|
+
Description: {description}
|
|
898
|
+
|
|
899
|
+
Follow these strict rules:
|
|
900
|
+
- Use DOMQL v3 syntax ONLY (extends, childExtends, flattened props, onX events)
|
|
901
|
+
- Components are plain objects with named exports: export const {component_name} = {{ ... }}
|
|
902
|
+
- Use design-system tokens for spacing (A, B, C), colors, typography
|
|
903
|
+
- NO imports between files — reference components by PascalCase key name
|
|
904
|
+
- All folders flat — no subfolders
|
|
905
|
+
- Include responsive breakpoints (@mobile, @tablet) where appropriate
|
|
906
|
+
- Follow modern UI/UX: visual hierarchy, minimal cognitive load, confident typography
|
|
907
|
+
|
|
908
|
+
Output ONLY the JavaScript code."""
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
@mcp.prompt()
|
|
912
|
+
def symbols_migration_prompt(source_framework: str = "React") -> str:
|
|
913
|
+
"""Prompt template for migrating code to Symbols/DOMQL v3."""
|
|
914
|
+
return f"""You are migrating {source_framework} code to Symbols/DOMQL v3.
|
|
915
|
+
|
|
916
|
+
Key conversion rules for {source_framework}:
|
|
917
|
+
- Components become plain objects (never functions)
|
|
918
|
+
- NO imports between project files
|
|
919
|
+
- All folders are flat — no subfolders
|
|
920
|
+
- Use extends/childExtends (v3 plural, never v2 singular)
|
|
921
|
+
- Flatten all props directly (no props: {{}} wrapper)
|
|
922
|
+
- Events use onX prefix (no on: {{}} wrapper)
|
|
923
|
+
- Use design-system tokens for spacing/colors
|
|
924
|
+
- State: state: {{ key: val }} + s.update({{ key: newVal }})
|
|
925
|
+
- Effects: onRender for mount, onStateUpdate for dependency changes
|
|
926
|
+
- Lists: children: (el, s) => s.items, childrenAs: 'state', childExtends: 'Item'
|
|
927
|
+
|
|
928
|
+
Provide the {source_framework} code to convert and I will output clean DOMQL v3."""
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@mcp.prompt()
|
|
932
|
+
def symbols_project_prompt(description: str) -> str:
|
|
933
|
+
"""Prompt template for scaffolding a complete Symbols project."""
|
|
934
|
+
return f"""Create a complete Symbols/DOMQL v3 project:
|
|
935
|
+
|
|
936
|
+
Project Description: {description}
|
|
937
|
+
|
|
938
|
+
Required structure (smbls/ folder):
|
|
939
|
+
- index.js (root export)
|
|
940
|
+
- config.js (platform config)
|
|
941
|
+
- vars.js (global constants)
|
|
942
|
+
- dependencies.js (external packages)
|
|
943
|
+
- components/ (PascalCase files, named exports)
|
|
944
|
+
- pages/ (dash-case files, camelCase exports, route mapping in index.js)
|
|
945
|
+
- functions/ (camelCase, called via el.call())
|
|
946
|
+
- designSystem/ (color, spacing, typography, theme, icons)
|
|
947
|
+
- state/ (default exports)
|
|
948
|
+
|
|
949
|
+
Rules:
|
|
950
|
+
- v3 syntax only — extends, childExtends, flattened props, onX events
|
|
951
|
+
- Design tokens for all spacing/colors (padding: 'A', not padding: '16px')
|
|
952
|
+
- Components are plain objects, never functions
|
|
953
|
+
- No imports between project files
|
|
954
|
+
- All folders completely flat
|
|
955
|
+
|
|
956
|
+
Generate all files with complete, production-ready code."""
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@mcp.prompt()
|
|
960
|
+
def symbols_review_prompt() -> str:
|
|
961
|
+
"""Prompt template for reviewing Symbols/DOMQL code."""
|
|
962
|
+
return """Review this Symbols/DOMQL code for v3 compliance and best practices.
|
|
963
|
+
|
|
964
|
+
Check for these violations:
|
|
965
|
+
1. v2 syntax: extend→extends, childExtend→childExtends, props:{}, on:{}
|
|
966
|
+
2. Imports between project files (FORBIDDEN)
|
|
967
|
+
3. Function-based components (must be plain objects)
|
|
968
|
+
4. Subfolders (must be flat)
|
|
969
|
+
5. Hardcoded pixels instead of design tokens
|
|
970
|
+
6. Wrong event handler signatures
|
|
971
|
+
7. Default exports for components (should be named)
|
|
972
|
+
|
|
973
|
+
Provide:
|
|
974
|
+
- Issues found with line references
|
|
975
|
+
- Corrected code for each issue
|
|
976
|
+
- Overall v3 compliance score (1-10)
|
|
977
|
+
- Improvement suggestions
|
|
978
|
+
|
|
979
|
+
Paste your code below:"""
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
# ---------------------------------------------------------------------------
|
|
983
|
+
# Health Check for Railway
|
|
984
|
+
# ---------------------------------------------------------------------------
|
|
985
|
+
from fastapi import FastAPI
|
|
986
|
+
from fastapi.responses import JSONResponse
|
|
987
|
+
|
|
988
|
+
# Create FastAPI app for health check
|
|
989
|
+
app = FastAPI()
|
|
990
|
+
|
|
991
|
+
@app.get("/health")
|
|
992
|
+
async def health_check():
|
|
993
|
+
"""Health check endpoint for Railway."""
|
|
994
|
+
return JSONResponse({
|
|
995
|
+
"status": "healthy",
|
|
996
|
+
"server": "Symbols MCP Server",
|
|
997
|
+
"tools": len(mcp._tool_manager.list_tools()),
|
|
998
|
+
"resources": len(mcp._resource_manager.list_resources()),
|
|
999
|
+
"prompts": len(mcp._prompt_manager.list_prompts())
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
@app.get("/api/config")
|
|
1003
|
+
async def proxy_config():
|
|
1004
|
+
"""Expose Supabase credentials to MCP clients using the proxy."""
|
|
1005
|
+
supabase_url = os.getenv("SUPABASE_URL", "")
|
|
1006
|
+
supabase_key = os.getenv("SUPABASE_KEY", "")
|
|
1007
|
+
if not supabase_url or not supabase_key:
|
|
1008
|
+
return JSONResponse({"error": "Supabase not configured"}, status_code=404)
|
|
1009
|
+
return JSONResponse({"supabase_url": supabase_url, "supabase_key": supabase_key})
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@app.post("/api/chat")
|
|
1013
|
+
async def proxy_chat(request: dict):
|
|
1014
|
+
"""Proxy chat completions to OpenRouter (for users without API keys)."""
|
|
1015
|
+
api_key = os.getenv("OPENROUTER_API_KEY")
|
|
1016
|
+
if not api_key:
|
|
1017
|
+
return JSONResponse({"error": "Server configuration error"}, status_code=500)
|
|
1018
|
+
|
|
1019
|
+
# Validate request
|
|
1020
|
+
if not request or "messages" not in request:
|
|
1021
|
+
return JSONResponse({"error": "Invalid request - messages required"}, status_code=400)
|
|
1022
|
+
|
|
1023
|
+
try:
|
|
1024
|
+
async with httpx.AsyncClient() as client:
|
|
1025
|
+
response = await client.post(
|
|
1026
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
1027
|
+
headers={
|
|
1028
|
+
"Authorization": f"Bearer {api_key}",
|
|
1029
|
+
"Content-Type": "application/json",
|
|
1030
|
+
"HTTP-Referer": "https://github.com/baronsilver/symbols-mcp-server",
|
|
1031
|
+
},
|
|
1032
|
+
json={
|
|
1033
|
+
"model": request.get("model", "openai/gpt-4.1-mini"),
|
|
1034
|
+
"messages": request["messages"],
|
|
1035
|
+
"max_tokens": request.get("max_tokens", 4000),
|
|
1036
|
+
"temperature": request.get("temperature", 0.7),
|
|
1037
|
+
},
|
|
1038
|
+
timeout=60.0,
|
|
1039
|
+
)
|
|
1040
|
+
response.raise_for_status()
|
|
1041
|
+
return JSONResponse(response.json())
|
|
1042
|
+
except httpx.HTTPError as e:
|
|
1043
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
1044
|
+
|
|
1045
|
+
@app.get("/")
|
|
1046
|
+
async def root():
|
|
1047
|
+
"""Root endpoint - redirect to health."""
|
|
1048
|
+
return JSONResponse({
|
|
1049
|
+
"name": "Symbols MCP Server",
|
|
1050
|
+
"version": "1.0.0",
|
|
1051
|
+
"endpoints": {
|
|
1052
|
+
"health": "/health",
|
|
1053
|
+
"sse": "/sse"
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
@app.get("/sse")
|
|
1058
|
+
async def sse_endpoint():
|
|
1059
|
+
"""SSE endpoint for MCP HTTP transport."""
|
|
1060
|
+
from sse_starlette.sse import EventSourceResponse
|
|
1061
|
+
|
|
1062
|
+
async def event_generator():
|
|
1063
|
+
# Send server info
|
|
1064
|
+
yield {
|
|
1065
|
+
"event": "endpoint",
|
|
1066
|
+
"data": json.dumps({
|
|
1067
|
+
"jsonrpc": "2.0",
|
|
1068
|
+
"method": "endpoint",
|
|
1069
|
+
"params": {
|
|
1070
|
+
"uri": "/message"
|
|
1071
|
+
}
|
|
1072
|
+
})
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return EventSourceResponse(event_generator())
|
|
1076
|
+
|
|
1077
|
+
@app.post("/message")
|
|
1078
|
+
async def message_endpoint(request: dict):
|
|
1079
|
+
"""Message endpoint for MCP HTTP transport."""
|
|
1080
|
+
return JSONResponse({
|
|
1081
|
+
"jsonrpc": "2.0",
|
|
1082
|
+
"id": request.get("id"),
|
|
1083
|
+
"result": {
|
|
1084
|
+
"server": {
|
|
1085
|
+
"name": "Symbols AI Assistant",
|
|
1086
|
+
"version": "1.0.0"
|
|
1087
|
+
},
|
|
1088
|
+
"capabilities": {
|
|
1089
|
+
"tools": {},
|
|
1090
|
+
"resources": {},
|
|
1091
|
+
"prompts": {}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
# ---------------------------------------------------------------------------
|
|
1097
|
+
# Entry point
|
|
1098
|
+
# ---------------------------------------------------------------------------
|
|
1099
|
+
def main():
|
|
1100
|
+
"""Run the Symbols MCP server."""
|
|
1101
|
+
# Check if running in Railway (HTTP) or local (stdio)
|
|
1102
|
+
transport = os.getenv("RAILWAY_ENVIRONMENT", "stdio")
|
|
1103
|
+
if transport == "production":
|
|
1104
|
+
# Railway deployment - run FastAPI app directly
|
|
1105
|
+
import uvicorn
|
|
1106
|
+
port = int(os.getenv("PORT", 8080))
|
|
1107
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
1108
|
+
else:
|
|
1109
|
+
# Local development - use stdio transport
|
|
1110
|
+
mcp.run(transport="stdio")
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
if __name__ == "__main__":
|
|
1114
|
+
main()
|