@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.
Files changed (35) hide show
  1. package/.env.example +16 -0
  2. package/.env.railway +13 -0
  3. package/LICENSE +21 -0
  4. package/README.md +184 -0
  5. package/mcp.json +57 -0
  6. package/package.json +20 -0
  7. package/pyproject.toml +25 -0
  8. package/railway.toml +26 -0
  9. package/run.sh +17 -0
  10. package/symbols_mcp/__init__.py +1 -0
  11. package/symbols_mcp/server.py +1114 -0
  12. package/symbols_mcp/skills/ACCESSIBILITY.md +471 -0
  13. package/symbols_mcp/skills/ACCESSIBILITY_AUDITORY.md +70 -0
  14. package/symbols_mcp/skills/AGENT_INSTRUCTIONS.md +257 -0
  15. package/symbols_mcp/skills/BRAND_INDENTITY.md +69 -0
  16. package/symbols_mcp/skills/BUILT_IN_COMPONENTS.md +304 -0
  17. package/symbols_mcp/skills/CLAUDE.md +2158 -0
  18. package/symbols_mcp/skills/CLI_QUICK_START.md +205 -0
  19. package/symbols_mcp/skills/DESIGN_CRITIQUE.md +64 -0
  20. package/symbols_mcp/skills/DESIGN_DIRECTION.md +320 -0
  21. package/symbols_mcp/skills/DESIGN_SYSTEM_ARCHITECT.md +64 -0
  22. package/symbols_mcp/skills/DESIGN_SYSTEM_CONFIG.md +487 -0
  23. package/symbols_mcp/skills/DESIGN_SYSTEM_IN_PROPS.md +136 -0
  24. package/symbols_mcp/skills/DESIGN_TO_CODE.md +64 -0
  25. package/symbols_mcp/skills/DESIGN_TREND.md +50 -0
  26. package/symbols_mcp/skills/DOMQL_v2-v3_MIGRATION.md +236 -0
  27. package/symbols_mcp/skills/FIGMA_MATCHING.md +63 -0
  28. package/symbols_mcp/skills/GARY_TAN.md +80 -0
  29. package/symbols_mcp/skills/MARKETING_ASSETS.md +66 -0
  30. package/symbols_mcp/skills/MIGRATE_TO_SYMBOLS.md +614 -0
  31. package/symbols_mcp/skills/QUICKSTART.md +79 -0
  32. package/symbols_mcp/skills/SYMBOLS_LOCAL_INSTRUCTIONS.md +1405 -0
  33. package/symbols_mcp/skills/THE_PRESENTATION.md +69 -0
  34. package/symbols_mcp/skills/UI_UX_PATTERNS.md +68 -0
  35. 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()