@symbo.ls/mcp 1.0.13 → 1.0.17

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.
@@ -3,8 +3,15 @@
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const readline = require('readline')
6
+ const https = require('https')
7
+ const http = require('http')
6
8
 
7
9
  const SKILLS_DIR = path.join(__dirname, '..', 'symbols_mcp', 'skills')
10
+ const API_BASE = process.env.SYMBOLS_API_URL || 'https://api.symbols.app'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers — skills
14
+ // ---------------------------------------------------------------------------
8
15
 
9
16
  function readSkill(filename) {
10
17
  const p = path.join(SKILLS_DIR, filename)
@@ -37,6 +44,382 @@ function searchDocs(query, maxResults = 3) {
37
44
  return results.length ? JSON.stringify(results, null, 2) : `No results found for '${query}'`
38
45
  }
39
46
 
47
+ // ---------------------------------------------------------------------------
48
+ // Helpers — API
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const AUTH_HELP = `To authenticate, provide one of:
52
+ - **token**: JWT from \`smbls login\` (stored in ~/.smblsrc) or env var SYMBOLS_TOKEN
53
+ - **api_key**: API key (sk_live_...) from your project's integration settings
54
+
55
+ To get a token:
56
+ 1. Run \`smbls login\` in your terminal, or
57
+ 2. Use the \`login\` tool with your email and password`
58
+
59
+ function apiRequest(method, urlPath, { token, apiKey, body } = {}) {
60
+ return new Promise((resolve, reject) => {
61
+ const url = new URL(API_BASE + urlPath)
62
+ const isHttps = url.protocol === 'https:'
63
+ const options = {
64
+ hostname: url.hostname,
65
+ port: url.port || (isHttps ? 443 : 80),
66
+ path: url.pathname + url.search,
67
+ method,
68
+ headers: { 'Content-Type': 'application/json' }
69
+ }
70
+ if (apiKey) options.headers['Authorization'] = `ApiKey ${apiKey}`
71
+ else if (token) options.headers['Authorization'] = `Bearer ${token}`
72
+
73
+ const payload = body ? JSON.stringify(body) : null
74
+ if (payload) options.headers['Content-Length'] = Buffer.byteLength(payload)
75
+
76
+ const req = (isHttps ? https : http).request(options, res => {
77
+ let data = ''
78
+ res.on('data', chunk => { data += chunk })
79
+ res.on('end', () => {
80
+ try { resolve(JSON.parse(data)) }
81
+ catch { resolve({ success: false, error: `HTTP ${res.statusCode}`, message: data }) }
82
+ })
83
+ })
84
+ req.on('error', err => reject(err))
85
+ req.setTimeout(30000, () => { req.destroy(); reject(new Error('Request timeout')) })
86
+ if (payload) req.write(payload)
87
+ req.end()
88
+ })
89
+ }
90
+
91
+ function requireAuth(token, apiKey) {
92
+ if (!token && !apiKey) return `Authentication required.\n\n${AUTH_HELP}`
93
+ return null
94
+ }
95
+
96
+ async function resolveProjectId(project, token, apiKey) {
97
+ if (!project) return { id: '', error: 'Project identifier is required.' }
98
+ const isKey = project.startsWith('pr_') || !/^[0-9a-f]+$/.test(project)
99
+ if (isKey) {
100
+ const result = await apiRequest('GET', `/core/projects/key/${project}`, { token, apiKey })
101
+ if (result.success) return { id: result.data?._id || '', error: null }
102
+ return { id: '', error: `Project '${project}' not found: ${result.error || 'unknown error'}` }
103
+ }
104
+ return { id: project, error: null }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Helpers — JS-to-JSON conversion (mirrors frank pipeline)
109
+ // ---------------------------------------------------------------------------
110
+
111
+ const DATA_KEYS = ['components', 'pages', 'snippets', 'functions', 'methods',
112
+ 'designSystem', 'state', 'dependencies', 'files', 'config']
113
+ const CODE_SECTIONS = new Set(['components', 'pages', 'functions', 'methods', 'snippets'])
114
+
115
+ function findObjectEnd(code, start) {
116
+ if (start >= code.length) return -1
117
+ let i = start
118
+ while (i < code.length && ' \t\n\r'.includes(code[i])) i++
119
+ if (i >= code.length || !'{['.includes(code[i])) return -1
120
+ const opener = code[i]
121
+ const closer = opener === '{' ? '}' : ']'
122
+ let depth = 1
123
+ i++
124
+ let inString = null, inTemplate = false, escaped = false
125
+ while (i < code.length && depth > 0) {
126
+ const ch = code[i]
127
+ if (escaped) { escaped = false; i++; continue }
128
+ if (ch === '\\') { escaped = true; i++; continue }
129
+ if (inString) { if (ch === inString) inString = null; i++; continue }
130
+ if (inTemplate) { if (ch === '`') inTemplate = false; i++; continue }
131
+ if (ch === "'" || ch === '"') inString = ch
132
+ else if (ch === '`') inTemplate = true
133
+ else if (ch === opener) depth++
134
+ else if (ch === closer) depth--
135
+ i++
136
+ }
137
+ return depth === 0 ? i : -1
138
+ }
139
+
140
+ function parseJsToJson(sourceCode) {
141
+ const result = {}
142
+ const code = sourceCode.replace(/^\s*import\s+.*$/gm, '')
143
+ const stripped = code.trim()
144
+ if (stripped.startsWith('{')) {
145
+ try { return JSON.parse(stripped) } catch {}
146
+ }
147
+ const exportRe = /export\s+const\s+(\w+)\s*=\s*/g
148
+ let m
149
+ const matches = []
150
+ while ((m = exportRe.exec(code)) !== null) matches.push(m)
151
+ for (const match of matches) {
152
+ const name = match[1]
153
+ const start = match.index + match[0].length
154
+ const end = findObjectEnd(code, start)
155
+ if (end === -1) continue
156
+ result[name] = code.slice(start, end).trim()
157
+ }
158
+ if (!matches.length) {
159
+ const defMatch = /export\s+default\s+/.exec(code)
160
+ if (defMatch) {
161
+ const start = defMatch.index + defMatch[0].length
162
+ const end = findObjectEnd(code, start)
163
+ if (end !== -1) result['__default__'] = code.slice(start, end).trim()
164
+ }
165
+ }
166
+ return result
167
+ }
168
+
169
+ function jsObjToJson(rawJs) {
170
+ let s = rawJs.trim()
171
+ // Stringify function values
172
+ s = stringifyFunctionsInJs(s)
173
+ // Normalize quotes
174
+ s = normalizeQuotes(s)
175
+ // Quote unquoted keys
176
+ s = s.replace(/(?<=[\{,\n])\s*([a-zA-Z_$][\w$]*)\s*:/g, ' "$1":')
177
+ s = s.replace(/(?<=[\{,\n])\s*(@[\w$]+)\s*:/g, ' "$1":')
178
+ // Remove trailing commas
179
+ s = s.replace(/,\s*([}\]])/g, '$1')
180
+ try { return JSON.parse(s) } catch { return s }
181
+ }
182
+
183
+ function stringifyFunctionsInJs(code) {
184
+ const result = []
185
+ let i = 0
186
+ while (i < code.length) {
187
+ const rest = code.slice(i)
188
+ const arrowMatch = rest.match(/^(\([^)]*\)\s*=>|\w+\s*=>)\s*/)
189
+ const funcMatch = rest.match(/^function\s*\w*\s*\(/)
190
+ if (arrowMatch && isValuePosition(code, i)) {
191
+ const fnEnd = findFunctionEnd(code, i, true)
192
+ if (fnEnd > i) {
193
+ result.push(JSON.stringify(code.slice(i, fnEnd).trim()))
194
+ i = fnEnd; continue
195
+ }
196
+ }
197
+ if (funcMatch && isValuePosition(code, i)) {
198
+ const fnEnd = findFunctionEnd(code, i, false)
199
+ if (fnEnd > i) {
200
+ result.push(JSON.stringify(code.slice(i, fnEnd).trim()))
201
+ i = fnEnd; continue
202
+ }
203
+ }
204
+ result.push(code[i]); i++
205
+ }
206
+ return result.join('')
207
+ }
208
+
209
+ function isValuePosition(code, pos) {
210
+ let j = pos - 1
211
+ while (j >= 0 && ' \t\n\r'.includes(code[j])) j--
212
+ return j >= 0 && ':=,['.includes(code[j])
213
+ }
214
+
215
+ function findFunctionEnd(code, start, isArrow) {
216
+ let i = start
217
+ if (isArrow) {
218
+ const arrowPos = code.indexOf('=>', i)
219
+ if (arrowPos === -1) return -1
220
+ i = arrowPos + 2
221
+ while (i < code.length && ' \t\n\r'.includes(code[i])) i++
222
+ if (i < code.length && code[i] === '{') return findObjectEnd(code, i)
223
+ let depth = 0
224
+ while (i < code.length) {
225
+ const ch = code[i]
226
+ if ('({['.includes(ch)) depth++
227
+ else if (')}]'.includes(ch)) { if (depth === 0) return i; depth-- }
228
+ else if (ch === ',' && depth === 0) return i
229
+ i++
230
+ }
231
+ return i
232
+ } else {
233
+ const brace = code.indexOf('{', i)
234
+ if (brace === -1) return -1
235
+ return findObjectEnd(code, brace)
236
+ }
237
+ }
238
+
239
+ function normalizeQuotes(s) {
240
+ const result = []
241
+ let i = 0
242
+ while (i < s.length) {
243
+ if (s[i] === "'") {
244
+ let j = i + 1
245
+ while (j < s.length) {
246
+ if (s[j] === '\\' && j + 1 < s.length) { j += 2; continue }
247
+ if (s[j] === "'") break
248
+ j++
249
+ }
250
+ const inner = s.slice(i + 1, j).replace(/"/g, '\\"')
251
+ result.push(`"${inner}"`); i = j + 1
252
+ } else if (s[i] === '"') {
253
+ let j = i + 1
254
+ while (j < s.length) {
255
+ if (s[j] === '\\' && j + 1 < s.length) { j += 2; continue }
256
+ if (s[j] === '"') break
257
+ j++
258
+ }
259
+ result.push(s.slice(i, j + 1)); i = j + 1
260
+ } else {
261
+ result.push(s[i]); i++
262
+ }
263
+ }
264
+ return result.join('')
265
+ }
266
+
267
+ function encodeSchemaCode(codeStr) {
268
+ return codeStr.replace(/\n/g, '/////n').replace(/`/g, '/////tilde')
269
+ }
270
+
271
+ function buildSchemaItem(section, key, value) {
272
+ const codeStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
273
+ const item = {
274
+ title: key, key, type: section,
275
+ code: encodeSchemaCode(`export default ${codeStr}`)
276
+ }
277
+ if (section === 'components' || section === 'pages') {
278
+ Object.assign(item, { settings: { gridOptions: {} }, props: {}, interactivity: [], dataTypes: [], error: null })
279
+ }
280
+ return item
281
+ }
282
+
283
+ function buildChangesAndSchema(data) {
284
+ const changes = [], granular = [], orders = []
285
+ for (const [sectionKey, sectionData] of Object.entries(data)) {
286
+ if (!DATA_KEYS.includes(sectionKey)) continue
287
+ if (typeof sectionData !== 'object' || sectionData === null || Array.isArray(sectionData)) {
288
+ changes.push(['update', [sectionKey], sectionData])
289
+ granular.push(['update', [sectionKey], sectionData])
290
+ continue
291
+ }
292
+ const sectionItemKeys = []
293
+ for (const [itemKey, itemValue] of Object.entries(sectionData)) {
294
+ const itemPath = [sectionKey, itemKey]
295
+ changes.push(['update', itemPath, itemValue])
296
+ sectionItemKeys.push(itemKey)
297
+ if (typeof itemValue === 'object' && itemValue !== null && !Array.isArray(itemValue)) {
298
+ const itemKeys = []
299
+ for (const [propKey, propValue] of Object.entries(itemValue)) {
300
+ granular.push(['update', [...itemPath, propKey], propValue])
301
+ itemKeys.push(propKey)
302
+ }
303
+ if (itemKeys.length) orders.push({ path: itemPath, keys: itemKeys })
304
+ } else {
305
+ granular.push(['update', itemPath, itemValue])
306
+ }
307
+ if (CODE_SECTIONS.has(sectionKey)) {
308
+ const schemaItem = buildSchemaItem(sectionKey, itemKey, itemValue)
309
+ const schemaPath = ['schema', sectionKey, itemKey]
310
+ changes.push(['update', schemaPath, schemaItem])
311
+ granular.push(['delete', [...schemaPath, 'code']])
312
+ for (const [sk, sv] of Object.entries(schemaItem)) {
313
+ granular.push(['update', [...schemaPath, sk], sv])
314
+ }
315
+ }
316
+ }
317
+ if (sectionItemKeys.length) orders.push({ path: [sectionKey], keys: sectionItemKeys })
318
+ }
319
+ return { changes, granular, orders }
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Audit helpers (deterministic rule checking — mirrors server.py)
324
+ // ---------------------------------------------------------------------------
325
+
326
+ const V2_PATTERNS = [
327
+ [/\bextend\s*:/g, "v2 syntax: use 'extends' (plural) instead of 'extend'"],
328
+ [/\bchildExtend\s*:/g, "v2 syntax: use 'childExtends' (plural) instead of 'childExtend'"],
329
+ [/\bon\s*:\s*\{/g, "v2 syntax: flatten event handlers with onX prefix (e.g. onClick) instead of on: {} wrapper"],
330
+ [/\bprops\s*:\s*\{(?!\s*\})/g, "v2 syntax: flatten props directly on the component instead of props: {} wrapper"],
331
+ ]
332
+
333
+ const RULE_CHECKS = [
334
+ [/\bimport\s+.*\bfrom\s+['"]\.\//, "FORBIDDEN: No imports between project files — reference components by PascalCase key name"],
335
+ [/\bexport\s+default\s+\{/, "Components should use named exports (export const Name = {}), not default exports"],
336
+ [/\bfunction\s+\w+\s*\(.*\)\s*\{[\s\S]*?return\s*\{/, "Components must be plain objects, not functions that return objects"],
337
+ [/\bextends\s*:\s*(?!['"])\w+/, "FORBIDDEN: extends must be a quoted string name (extends: 'Name'), not a variable reference — register in components/ and use string lookup (Rule 10)"],
338
+ [/extends\s*:\s*['"]Flex['"]/, "Replace extends: 'Flex' with flow: 'x' or flow: 'y' — do NOT just remove it, the element needs flow to stay flex (Rule 26)"],
339
+ [/extends\s*:\s*['"]Box['"]/, "Remove extends: 'Box' — every element is already a Box (Rule 26)"],
340
+ [/extends\s*:\s*['"]Text['"]/, "Remove extends: 'Text' — any element with text: is already Text (Rule 26)"],
341
+ [/\bchildExtends\s*:\s*\{/, "FORBIDDEN: childExtends must be a quoted string name, not an inline object — register as a named component (Rule 10)"],
342
+ [/(?:padding|margin|gap|width|height|fontSize|borderRadius|minWidth|maxWidth|minHeight|maxHeight|top|left|right|bottom|letterSpacing|lineHeight|borderWidth|outlineWidth)\s*:\s*['"]?\d+(?:\.\d+)?px/, "FORBIDDEN: No raw px values — use design system tokens (A, B, C, etc.) instead of hardcoded pixels (Rule 28)"],
343
+ [/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]#[0-9a-fA-F]/, "Use design system color tokens (primary, secondary, white, gray.5) instead of hardcoded hex colors (Rule 27)"],
344
+ [/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]rgb/, "Use design system color tokens instead of hardcoded rgb/rgba values (Rule 27)"],
345
+ [/(?:color|background|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]hsl/, "Use design system color tokens instead of hardcoded hsl/hsla values (Rule 27)"],
346
+ [/<svg[\s>]/, "FORBIDDEN: Use the Icon component for SVG icons — store SVGs in designSystem/icons, never inline (Rule 29)"],
347
+ [/tag\s*:\s*['"]svg['"]/, "FORBIDDEN: Never use tag: 'svg' — store SVGs in designSystem/icons and use Icon component (Rule 29)"],
348
+ [/tag\s*:\s*['"]path['"]/, "FORBIDDEN: Never use tag: 'path' — store SVG paths in designSystem/icons and use Icon component (Rule 29)"],
349
+ [/extends\s*:\s*['"]Svg['"]/, "Use Icon component for icons, not Svg — Svg is only for decorative/structural SVGs (Rule 29)"],
350
+ [/\biconName\s*:/, "FORBIDDEN: Use icon: not iconName: — the prop is icon: 'name' matching a key in designSystem/icons (Rule 29)"],
351
+ [/document\.createElement\b/, "FORBIDDEN: No direct DOM manipulation — use DOMQL declarative object syntax instead (Rule 30)"],
352
+ [/\.querySelector\b/, "FORBIDDEN: No DOM queries — reference elements by key name in the DOMQL object tree (Rule 30)"],
353
+ [/\.appendChild\b/, "FORBIDDEN: No direct DOM manipulation — nest children as object keys or use children array (Rule 30)"],
354
+ [/\.removeChild\b/, "FORBIDDEN: No direct DOM manipulation — use if: (el, s) => condition to show/hide (Rule 30)"],
355
+ [/\.insertBefore\b/, "FORBIDDEN: No direct DOM manipulation — use children array ordering (Rule 30)"],
356
+ [/\.innerHTML\s*=/, "FORBIDDEN: No direct DOM manipulation — use text: or html: prop (Rule 30)"],
357
+ [/\.classList\./, "FORBIDDEN: No direct class manipulation — use isX + '.isX' pattern (Rule 19/30)"],
358
+ [/\.setAttribute\b/, "FORBIDDEN: No direct DOM manipulation — set attributes at root level in DOMQL (Rule 30)"],
359
+ [/\.addEventListener\b/, "FORBIDDEN: No direct event binding — use onX props: onClick, onInput, etc. (Rule 30)"],
360
+ [/\.style\.\w+\s*=/, "FORBIDDEN: No direct style manipulation — use DOMQL CSS-in-props (Rule 30)"],
361
+ [/html\s*:\s*\(?.*\)?\s*=>\s*/, "FORBIDDEN: Never use html: as a function returning markup — use DOMQL children, nesting, text:, and if: instead (Rule 31)"],
362
+ [/return\s*`<\w+/, "FORBIDDEN: Never return HTML template literals — use DOMQL declarative children and nesting (Rule 31)"],
363
+ [/style\s*=\s*['"`]/, "FORBIDDEN: No inline style= strings in html — use DOMQL CSS-in-props (Rule 31)"],
364
+ [/window\.innerWidth/, "FORBIDDEN: No window.innerWidth checks — use @mobileL, @tabletS responsive breakpoints (Rule 31)"],
365
+ [/\.parentNode\b/, "FORBIDDEN: No DOM traversal — use state and reactive props instead of walking the DOM tree (Rule 32)"],
366
+ [/\.childNodes\b/, "FORBIDDEN: No DOM traversal — use state-driven children with if: props (Rule 32)"],
367
+ [/\.textContent\b/, "FORBIDDEN: No DOM property access — use state and text: prop (Rule 32)"],
368
+ [/Array\.from\(\w+\.children\)/, "FORBIDDEN: No DOM child iteration — use state arrays with children/childExtends and if: filtering (Rule 32)"],
369
+ [/\.style\.display\s*=/, "FORBIDDEN: No style.display toggling — use show:/hide: to toggle visibility or if: to remove from DOM (Rule 32)"],
370
+ [/\.style\.cssText\s*=/, "FORBIDDEN: No direct cssText — use DOMQL CSS-in-props (Rule 32)"],
371
+ [/\.dataset\./, "FORBIDDEN: No dataset manipulation — use state and attr: for data-* attributes (Rule 32)"],
372
+ [/\.remove\(\)/, "FORBIDDEN: No DOM node removal — use if: (el, s) => condition to conditionally render (Rule 32)"],
373
+ [/el\.node\.\w+\s*=/, "FORBIDDEN: No direct el.node property assignment — use DOMQL props (placeholder:, value:, text:, etc.). Reading el.node is fine (Rule 39), writing is not (Rule 32)"],
374
+ [/document\.getElementById\b/, "FORBIDDEN: No document.getElementById — use el.lookdown('key') to find DOMQL elements (Rule 40)"],
375
+ [/document\.querySelectorAll\b/, "FORBIDDEN: No document.querySelectorAll — use el.lookdownAll('key') to find DOMQL elements (Rule 40)"],
376
+ [/el\.parent\.state\b/, "FORBIDDEN: Never use el.parent.state — with childrenAs: 'state', use s.field directly (Rule 36)"],
377
+ [/el\.context\.designSystem\b/, "FORBIDDEN: Never read designSystem from el.context in props — use token strings directly (Rule 38)"],
378
+ [/^const\s+\w+\s*=\s*(?:\(|function)/m, "FORBIDDEN: No module-level helper functions — move to functions/ and call via el.call('fnName') (Rule 33)"],
379
+ [/^let\s+\w+\s*=/m, "FORBIDDEN: No module-level variables — use el.scope for local state, functions/ for helpers (Rule 33)"],
380
+ [/^var\s+\w+\s*=/m, "FORBIDDEN: No module-level variables — use el.scope for local state, functions/ for helpers (Rule 33)"],
381
+ ]
382
+
383
+ function auditCode(code) {
384
+ const violations = []
385
+ const warnings = []
386
+
387
+ for (const [pattern, message] of V2_PATTERNS) {
388
+ const re = new RegExp(pattern.source, pattern.flags)
389
+ let m
390
+ while ((m = re.exec(code)) !== null) {
391
+ const line = code.slice(0, m.index).split('\n').length
392
+ violations.push({ line, severity: 'error', message })
393
+ }
394
+ }
395
+
396
+ for (const [pattern, message] of RULE_CHECKS) {
397
+ const re = new RegExp(pattern.source, pattern.flags || 'g')
398
+ let m
399
+ while ((m = re.exec(code)) !== null) {
400
+ const line = code.slice(0, m.index).split('\n').length
401
+ const level = message.includes('FORBIDDEN') ? 'error' : 'warning'
402
+ const target = level === 'error' ? violations : warnings
403
+ target.push({ line, severity: level, message })
404
+ }
405
+ }
406
+
407
+ const totalIssues = violations.length + warnings.length
408
+ const score = Math.max(1, 10 - totalIssues)
409
+
410
+ return {
411
+ passed: violations.length === 0,
412
+ score,
413
+ violations,
414
+ warnings,
415
+ summary: `${violations.length} errors, ${warnings.length} warnings — compliance score: ${score}/10`
416
+ }
417
+ }
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Tool definitions
421
+ // ---------------------------------------------------------------------------
422
+
40
423
  const TOOLS = [
41
424
  {
42
425
  name: 'get_project_rules',
@@ -45,23 +428,479 @@ const TOOLS = [
45
428
  },
46
429
  {
47
430
  name: 'search_symbols_docs',
48
- description: 'Search the Symbols documentation knowledge base for relevant information.',
431
+ description: 'Search the Symbols documentation knowledge base for relevant information including CLI commands, SDK services, syntax, components, and more.',
49
432
  inputSchema: {
50
433
  type: 'object',
51
434
  properties: {
52
- query: { type: 'string', description: 'Natural language search query about Symbols/DOMQL' },
435
+ query: { type: 'string', description: 'Natural language search query about Symbols/DOMQL/CLI/SDK' },
53
436
  max_results: { type: 'number', description: 'Maximum number of results (1-5)', default: 3 }
54
437
  },
55
438
  required: ['query']
56
439
  }
440
+ },
441
+ {
442
+ name: 'get_cli_reference',
443
+ description: 'Returns the complete Symbols CLI (@symbo.ls/cli) command reference — all smbls commands, options, and workflows.',
444
+ inputSchema: { type: 'object', properties: {} }
445
+ },
446
+ {
447
+ name: 'get_sdk_reference',
448
+ description: 'Returns the complete Symbols SDK (@symbo.ls/sdk) API reference — all services, methods, and usage examples.',
449
+ inputSchema: { type: 'object', properties: {} }
450
+ },
451
+ {
452
+ name: 'generate_component',
453
+ description: 'Generate a Symbols.app DOMQL v3 component from a description with full context (rules, syntax, cookbook, default library).',
454
+ inputSchema: {
455
+ type: 'object',
456
+ properties: {
457
+ description: { type: 'string', description: 'What the component should do and look like' },
458
+ component_name: { type: 'string', description: 'PascalCase name for the component', default: 'MyComponent' }
459
+ },
460
+ required: ['description']
461
+ }
462
+ },
463
+ {
464
+ name: 'generate_page',
465
+ description: 'Generate a Symbols.app page with routing integration and full context.',
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ description: { type: 'string', description: 'What the page should contain and do' },
470
+ page_name: { type: 'string', description: 'camelCase name for the page', default: 'home' }
471
+ },
472
+ required: ['description']
473
+ }
474
+ },
475
+ {
476
+ name: 'convert_react',
477
+ description: 'Convert React/JSX code to Symbols.app DOMQL v3 with migration context.',
478
+ inputSchema: {
479
+ type: 'object',
480
+ properties: {
481
+ source_code: { type: 'string', description: 'The React/JSX source code to convert' }
482
+ },
483
+ required: ['source_code']
484
+ }
485
+ },
486
+ {
487
+ name: 'convert_html',
488
+ description: 'Convert raw HTML/CSS to Symbols.app DOMQL v3 components with full context.',
489
+ inputSchema: {
490
+ type: 'object',
491
+ properties: {
492
+ source_code: { type: 'string', description: 'The HTML/CSS source code to convert' }
493
+ },
494
+ required: ['source_code']
495
+ }
496
+ },
497
+ {
498
+ name: 'detect_environment',
499
+ description: 'Detect project type (local, CDN, JSON runtime, or remote server) based on project indicators.',
500
+ inputSchema: {
501
+ type: 'object',
502
+ properties: {
503
+ has_symbols_json: { type: 'boolean', default: false },
504
+ has_symbols_dir: { type: 'boolean', default: false },
505
+ has_package_json: { type: 'boolean', default: false },
506
+ has_cdn_import: { type: 'boolean', default: false },
507
+ has_iife_script: { type: 'boolean', default: false },
508
+ has_json_data: { type: 'boolean', default: false },
509
+ has_mermaid_config: { type: 'boolean', default: false },
510
+ file_list: { type: 'string', description: 'Comma-separated list of key files', default: '' }
511
+ }
512
+ }
513
+ },
514
+ {
515
+ name: 'audit_component',
516
+ description: 'Audit a Symbols/DOMQL component for v3 compliance — checks for v2 syntax, raw px values, hardcoded colors, direct DOM manipulation, and more. Returns violations, warnings, and a score.',
517
+ inputSchema: {
518
+ type: 'object',
519
+ properties: {
520
+ component_code: { type: 'string', description: 'The JavaScript component code to audit' }
521
+ },
522
+ required: ['component_code']
523
+ }
524
+ },
525
+ {
526
+ name: 'convert_to_json',
527
+ description: 'Convert DOMQL v3 JavaScript source code to platform JSON format. Parses export statements, stringifies functions, outputs JSON ready for save_to_project.',
528
+ inputSchema: {
529
+ type: 'object',
530
+ properties: {
531
+ source_code: { type: 'string', description: 'JavaScript source code with export const/default statements' },
532
+ section: { type: 'string', description: 'Target section: components, pages, functions, snippets, designSystem, state', default: 'components' }
533
+ },
534
+ required: ['source_code']
535
+ }
536
+ },
537
+ {
538
+ name: 'login',
539
+ description: 'Log in to the Symbols platform and get an access token for project operations.',
540
+ inputSchema: {
541
+ type: 'object',
542
+ properties: {
543
+ email: { type: 'string', description: 'Symbols account email address' },
544
+ password: { type: 'string', description: 'Symbols account password' }
545
+ },
546
+ required: ['email', 'password']
547
+ }
548
+ },
549
+ {
550
+ name: 'list_projects',
551
+ description: 'List the user\'s Symbols projects (names, keys, IDs) to choose from.',
552
+ inputSchema: {
553
+ type: 'object',
554
+ properties: {
555
+ token: { type: 'string', description: 'JWT access token from login' },
556
+ api_key: { type: 'string', description: 'API key (sk_live_...)' }
557
+ }
558
+ }
559
+ },
560
+ {
561
+ name: 'create_project',
562
+ description: 'Create a new Symbols project on the platform.',
563
+ inputSchema: {
564
+ type: 'object',
565
+ properties: {
566
+ name: { type: 'string', description: 'Project display name' },
567
+ key: { type: 'string', description: 'Project key (pr_xxxx format). Auto-generated if empty' },
568
+ token: { type: 'string', description: 'JWT access token from login' },
569
+ api_key: { type: 'string', description: 'API key (sk_live_...)' },
570
+ visibility: { type: 'string', description: 'private, public, or password-protected', default: 'private' }
571
+ },
572
+ required: ['name']
573
+ }
574
+ },
575
+ {
576
+ name: 'get_project',
577
+ description: 'Get a project\'s current data (components, pages, design system, state).',
578
+ inputSchema: {
579
+ type: 'object',
580
+ properties: {
581
+ project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
582
+ token: { type: 'string', description: 'JWT access token from login' },
583
+ api_key: { type: 'string', description: 'API key (sk_live_...)' },
584
+ branch: { type: 'string', description: 'Branch to read from', default: 'main' }
585
+ },
586
+ required: ['project']
587
+ }
588
+ },
589
+ {
590
+ name: 'save_to_project',
591
+ description: 'Save components/pages/data to a Symbols project. Creates a new version with change tuples, granular changes, orders, and auto-generated schema entries (mirrors CLI push pipeline).',
592
+ inputSchema: {
593
+ type: 'object',
594
+ properties: {
595
+ project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
596
+ changes: { type: 'string', description: 'JSON string with project data: { components: {...}, pages: {...}, designSystem: {...}, state: {...}, functions: {...} }' },
597
+ token: { type: 'string', description: 'JWT access token from login' },
598
+ api_key: { type: 'string', description: 'API key (sk_live_...)' },
599
+ message: { type: 'string', description: 'Version commit message' },
600
+ branch: { type: 'string', description: 'Branch to save to', default: 'main' }
601
+ },
602
+ required: ['project', 'changes']
603
+ }
604
+ },
605
+ {
606
+ name: 'publish',
607
+ description: 'Publish a version of a Symbols project. Makes the specified version (or latest) the published/live version.',
608
+ inputSchema: {
609
+ type: 'object',
610
+ properties: {
611
+ project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
612
+ token: { type: 'string', description: 'JWT access token from login' },
613
+ api_key: { type: 'string', description: 'API key (sk_live_...)' },
614
+ version: { type: 'string', description: 'Version string or ID. Empty for latest' },
615
+ branch: { type: 'string', description: 'Branch to publish from', default: 'main' }
616
+ },
617
+ required: ['project']
618
+ }
619
+ },
620
+ {
621
+ name: 'push',
622
+ description: 'Deploy a Symbols project to an environment (production, staging, dev).',
623
+ inputSchema: {
624
+ type: 'object',
625
+ properties: {
626
+ project: { type: 'string', description: 'Project key (pr_xxxx) or project ID' },
627
+ token: { type: 'string', description: 'JWT access token from login' },
628
+ api_key: { type: 'string', description: 'API key (sk_live_...)' },
629
+ environment: { type: 'string', description: 'Target environment key', default: 'production' },
630
+ mode: { type: 'string', description: 'Deploy mode: latest, published, version, or branch', default: 'published' },
631
+ version: { type: 'string', description: 'Version string when mode is "version"' },
632
+ branch: { type: 'string', description: 'Branch when mode is "latest" or "branch"', default: 'main' }
633
+ },
634
+ required: ['project']
635
+ }
57
636
  }
58
637
  ]
59
638
 
639
+ // ---------------------------------------------------------------------------
640
+ // Tool handlers
641
+ // ---------------------------------------------------------------------------
642
+
643
+ async function handleTool(name, args) {
644
+ // Documentation tools (sync)
645
+ if (name === 'get_project_rules') return loadAgentInstructions()
646
+ if (name === 'search_symbols_docs') return searchDocs(args.query, args.max_results || 3)
647
+ if (name === 'get_cli_reference') return readSkill('CLI.md')
648
+ if (name === 'get_sdk_reference') return readSkill('SDK.md')
649
+
650
+ // Helper to read and concatenate multiple skill files
651
+ function readSkills(...filenames) {
652
+ return filenames.map(f => readSkill(f)).filter(c => !c.startsWith('Skill ')).join('\n\n---\n\n')
653
+ }
654
+
655
+ // generate_component
656
+ if (name === 'generate_component') {
657
+ const componentName = args.component_name || 'MyComponent'
658
+ const context = readSkills('RULES.md', 'COMMON_MISTAKES.md', 'COMPONENTS.md', 'SYNTAX.md', 'COOKBOOK.md', 'DEFAULT_LIBRARY.md')
659
+ return `# Generate Component: ${componentName}\n\n## Description\n${args.description}\n\n## Requirements\n- Named export: \`export const ${componentName} = { ... }\`\n- DOMQL v3 syntax only (extends, childExtends, flattened props, onX events)\n- **MANDATORY: ALL values MUST use design system tokens** — spacing (A, B, C, D), colors (primary, surface, white, gray.5), typography (fontSize: 'B'). ZERO px values, ZERO hex colors, ZERO rgb/hsl.\n- NO imports between files — PascalCase keys auto-extend registered components\n- Include responsive breakpoints where appropriate (@tabletS, @mobileL)\n- Use the default library components (Button, Avatar, Icon, Field, etc.) via extends\n- Use Icon component for SVGs — store icons in designSystem/icons\n- NO direct DOM manipulation — all structure via DOMQL declarative syntax\n- Follow modern UI/UX: visual hierarchy, confident typography, minimal cognitive load\n\n## Context — Rules, Syntax & Examples\n\n${context}`
660
+ }
661
+
662
+ // generate_page
663
+ if (name === 'generate_page') {
664
+ const pageName = args.page_name || 'home'
665
+ const context = readSkills('RULES.md', 'COMMON_MISTAKES.md', 'PROJECT_STRUCTURE.md', 'PATTERNS.md', 'SNIPPETS.md', 'DEFAULT_LIBRARY.md', 'COMPONENTS.md')
666
+ return `# Generate Page: ${pageName}\n\n## Description\n${args.description}\n\n## Requirements\n- Export as: \`export const ${pageName} = { ... }\`\n- Page is a plain object composing components\n- Add to pages/index.js route map: \`'/${pageName}': ${pageName}\`\n- Use components by PascalCase key (Header, Footer, Hero, etc.)\n- **MANDATORY: ALL values MUST use design system tokens** — spacing (A, B, C, D), colors (primary, surface, white, gray.5), typography (fontSize: 'B'). ZERO px values, ZERO hex colors, ZERO rgb/hsl.\n- Use Icon component for SVGs — store icons in designSystem/icons\n- NO direct DOM manipulation — all structure via DOMQL declarative syntax\n- Include responsive layout adjustments\n\n## Context — Rules, Structure, Patterns & Snippets\n\n${context}`
667
+ }
668
+
669
+ // convert_react
670
+ if (name === 'convert_react') {
671
+ const context = readSkills('RULES.md', 'MIGRATION.md', 'SYNTAX.md', 'COMPONENTS.md', 'LEARNINGS.md')
672
+ return `# Convert React → Symbols DOMQL v3\n\n## Source Code to Convert\n\`\`\`jsx\n${args.source_code}\n\`\`\`\n\n## Conversion Rules\n- Function/class components → plain object exports\n- JSX → nested object children (PascalCase keys auto-extend)\n- import/export between files → REMOVE (reference by key name)\n- useState → state: { key: val } + s.update({ key: newVal })\n- useEffect → onRender (mount), onStateUpdate (deps)\n- props → flattened directly on component (no props wrapper)\n- onClick={handler} → onClick: (event, el, state) => {}\n- className → use design tokens and theme directly\n- map() → children: (el, s) => s.items, childExtends, childProps\n- conditional rendering → if: (el, s) => boolean\n- CSS modules/styled → CSS-in-props with design tokens\n- React.Fragment → not needed, just nest children\n\n## Context — Migration Guide, Syntax & Rules\n\n${context}`
673
+ }
674
+
675
+ // convert_html
676
+ if (name === 'convert_html') {
677
+ const context = readSkills('RULES.md', 'SYNTAX.md', 'COMPONENTS.md', 'DESIGN_SYSTEM.md', 'SNIPPETS.md', 'LEARNINGS.md')
678
+ return `# Convert HTML → Symbols DOMQL v3\n\n## Source Code to Convert\n\`\`\`html\n${args.source_code}\n\`\`\`\n\n## Conversion Rules\n- <div> → Box, Flex, or Grid (based on layout purpose)\n- <span>, <p>, <h1>-<h6> → Text, P, H with tag property\n- <a> → Link (has built-in SPA router)\n- <button> → Button (has icon/text support)\n- <input> → Input, Radio, Checkbox (based on type)\n- <img> → Img\n- <form> → Form (extends Box with tag: 'form')\n- <ul>/<ol> + <li> → children array with childExtends\n- CSS classes → flatten as CSS-in-props on the component\n- CSS px values → design tokens (16px → 'A', 26px → 'B', 42px → 'C')\n- CSS colors → theme color tokens\n- media queries → @tabletS, @mobileL, @screenS breakpoints\n- id/class attributes → not needed (use key names and themes)\n- inline styles → flatten as component properties\n- <style> blocks → distribute to component-level properties\n\n## Context — Syntax, Components & Design System\n\n${context}`
679
+ }
680
+
681
+ // detect_environment
682
+ if (name === 'detect_environment') {
683
+ let envType = 'unknown', confidence = 'low'
684
+ if (args.has_mermaid_config) { envType = 'remote_server'; confidence = 'high' }
685
+ else if (args.has_json_data) { envType = 'json_runtime'; confidence = 'high' }
686
+ else if (args.has_symbols_json && args.has_symbols_dir) { envType = 'local_project'; confidence = 'high' }
687
+ else if (args.has_symbols_dir || (args.has_package_json && args.has_symbols_json)) { envType = 'local_project'; confidence = 'medium' }
688
+ else if (args.has_cdn_import || args.has_iife_script) { envType = 'cdn'; confidence = 'high' }
689
+ else if (args.has_package_json) { envType = 'local_project'; confidence = 'low' }
690
+ else if (args.file_list) {
691
+ const files = args.file_list.toLowerCase()
692
+ if (files.includes('index.html') && !files.includes('package.json') && !files.includes('symbols.json')) { envType = 'cdn'; confidence = 'medium' }
693
+ }
694
+ const guide = readSkill('RUNNING_APPS.md')
695
+ return `# Environment Detection\n\n**Detected: ${envType}** (confidence: ${confidence})\n\n${guide}`
696
+ }
697
+
698
+ // audit_component (sync)
699
+ if (name === 'audit_component') {
700
+ const result = auditCode(args.component_code)
701
+ const rulesContext = readSkill('AUDIT.md')
702
+ let output = `# Audit Report\n\n## Summary\n${result.summary}\nPassed: ${result.passed ? 'Yes' : 'No'}\n\n## Violations (Errors)\n`
703
+ if (result.violations.length) {
704
+ for (const v of result.violations) output += `- **Line ${v.line}**: ${v.message}\n`
705
+ } else {
706
+ output += 'No violations found.\n'
707
+ }
708
+ output += '\n## Warnings\n'
709
+ if (result.warnings.length) {
710
+ for (const w of result.warnings) output += `- **Line ${w.line}**: ${w.message}\n`
711
+ } else {
712
+ output += 'No warnings.\n'
713
+ }
714
+ if (result.violations.length) {
715
+ output += '\n## MANDATORY ACTION\n\n**Every violation above MUST be fixed. There is NO concept of "known debt", "accepted violations", or "95% fixed" in Symbols. ALL violations must reach 100% resolution. Do NOT label any violation as "known debt" or defer it. Rewrite the code using proper DOMQL syntax.**\n\n'
716
+ }
717
+ output += `\n## Detailed Rules Reference\n\n${rulesContext}`
718
+ return output
719
+ }
720
+
721
+ // convert_to_json (sync)
722
+ if (name === 'convert_to_json') {
723
+ const section = args.section || 'components'
724
+ const parsed = parseJsToJson(args.source_code)
725
+ if (!Object.keys(parsed).length) {
726
+ return 'Could not parse any exports from the source code. Make sure it contains `export const Name = { ... }` or `export default { ... }`.'
727
+ }
728
+ const result = {}
729
+ for (const [exportName, rawValue] of Object.entries(parsed)) {
730
+ const converted = typeof rawValue === 'string' ? jsObjToJson(rawValue) : rawValue
731
+ if (exportName === '__default__') {
732
+ if (['designSystem', 'state', 'dependencies', 'config'].includes(section)) {
733
+ result[section] = converted
734
+ } else {
735
+ if (!result[section]) result[section] = {}
736
+ result[section]['default'] = converted
737
+ }
738
+ } else {
739
+ if (!result[section]) result[section] = {}
740
+ result[section][exportName] = converted
741
+ }
742
+ }
743
+ const output = JSON.stringify(result, null, 2)
744
+ const sections = Object.keys(result)
745
+ const items = []
746
+ for (const sec of sections) {
747
+ if (typeof result[sec] === 'object' && result[sec]) items.push(...Object.keys(result[sec]))
748
+ }
749
+ return `# Converted to Platform JSON\n\n**Section:** ${sections.join(', ')}\n**Items:** ${items.join(', ') || 'default export'}\n\n\`\`\`json\n${output}\n\`\`\`\n\nThis JSON is ready to use with \`save_to_project\`. Pass the JSON object above as the \`changes\` parameter.\n\n**Full flow:**\n1. \`convert_to_json\` (done) → structured JSON\n2. \`save_to_project\` → push to platform (creates new version)\n3. \`publish\` → make version live\n4. \`push\` → deploy to environment (production/staging/dev)`
750
+ }
751
+
752
+ // login
753
+ if (name === 'login') {
754
+ const result = await apiRequest('POST', '/core/auth/login', {
755
+ body: { email: args.email, password: args.password }
756
+ })
757
+ if (result.success) {
758
+ const { tokens = {}, user = {} } = result.data || {}
759
+ const token = tokens.accessToken || ''
760
+ return `Logged in as ${user.name || user.email || 'unknown'}.\nToken: ${token}\nExpires: ${tokens.accessTokenExp?.expiresAt || 'unknown'}\n\nUse this token with project, save, publish and push tools.`
761
+ }
762
+ return `Login failed: ${result.error || result.message || 'Unknown error'}`
763
+ }
764
+
765
+ // list_projects
766
+ if (name === 'list_projects') {
767
+ const authErr = requireAuth(args.token, args.api_key)
768
+ if (authErr) return authErr
769
+ const result = await apiRequest('GET', '/core/projects', { token: args.token, apiKey: args.api_key })
770
+ if (result.success) {
771
+ const projects = result.data || []
772
+ if (!projects.length) return 'No projects found. Use `create_project` to create one.'
773
+ const lines = ['# Your Projects\n']
774
+ for (const p of projects) {
775
+ lines.push(`- **${p.name || 'Untitled'}** — key: \`${p.key || '—'}\`, id: \`${p._id || ''}\`, visibility: ${p.visibility || 'private'}`)
776
+ }
777
+ return lines.join('\n')
778
+ }
779
+ return `Failed to list projects: ${result.error || 'Unknown error'}`
780
+ }
781
+
782
+ // create_project
783
+ if (name === 'create_project') {
784
+ const authErr = requireAuth(args.token, args.api_key)
785
+ if (authErr) return authErr
786
+ const body = { name: args.name, visibility: args.visibility || 'private', language: 'javascript' }
787
+ if (args.key) body.key = args.key
788
+ const result = await apiRequest('POST', '/core/projects', { token: args.token, apiKey: args.api_key, body })
789
+ if (result.success) {
790
+ const d = result.data || {}
791
+ return `Project created successfully.\nName: ${d.name || args.name}\nKey: \`${d.key || 'unknown'}\`\nID: \`${d._id || 'unknown'}\`\n\nUse this project key/ID with \`save_to_project\` to push your components.`
792
+ }
793
+ return `Create failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
794
+ }
795
+
796
+ // get_project
797
+ if (name === 'get_project') {
798
+ const authErr = requireAuth(args.token, args.api_key)
799
+ if (authErr) return authErr
800
+ const branch = args.branch || 'main'
801
+ const project = args.project
802
+ const isKey = project.startsWith('pr_') || !/^[0-9a-f]+$/.test(project)
803
+ const urlPath = isKey
804
+ ? `/core/projects/key/${project}/data?branch=${branch}&version=latest`
805
+ : `/core/projects/${project}/data?branch=${branch}&version=latest`
806
+ const result = await apiRequest('GET', urlPath, { token: args.token, apiKey: args.api_key })
807
+ if (result.success) {
808
+ const data = result.data || {}
809
+ const components = data.components || {}
810
+ const pages = data.pages || {}
811
+ const ds = data.designSystem || {}
812
+ const state = data.state || {}
813
+ const functions = data.functions || {}
814
+ const lines = [`# Project Data (branch: ${branch})\n`]
815
+ lines.push(`**Components (${Object.keys(components).length}):** ${Object.keys(components).slice(0, 20).join(', ') || 'none'}`)
816
+ lines.push(`**Pages (${Object.keys(pages).length}):** ${Object.keys(pages).slice(0, 20).join(', ') || 'none'}`)
817
+ lines.push(`**Design System keys:** ${Object.keys(ds).slice(0, 15).join(', ') || 'none'}`)
818
+ lines.push(`**State keys:** ${Object.keys(state).slice(0, 15).join(', ') || 'none'}`)
819
+ lines.push(`**Functions (${Object.keys(functions).length}):** ${Object.keys(functions).slice(0, 15).join(', ') || 'none'}`)
820
+ lines.push(`\n---\n\nFull data:\n\`\`\`json\n${JSON.stringify(data, null, 2).slice(0, 8000)}\n\`\`\``)
821
+ return lines.join('\n')
822
+ }
823
+ return `Failed to get project data: ${result.error || 'Unknown error'}`
824
+ }
825
+
826
+ // save_to_project
827
+ if (name === 'save_to_project') {
828
+ const authErr = requireAuth(args.token, args.api_key)
829
+ if (authErr) return authErr
830
+ let changesData
831
+ try { changesData = JSON.parse(args.changes) }
832
+ catch (e) { return `Invalid JSON in changes: ${e.message}` }
833
+ if (typeof changesData !== 'object' || changesData === null) {
834
+ return 'Changes must be a JSON object with keys like components, pages, designSystem, state, functions.'
835
+ }
836
+ const { id: projectId, error: resolveErr } = await resolveProjectId(args.project, args.token, args.api_key)
837
+ if (resolveErr) return resolveErr
838
+ const { changes, granular: granularChanges, orders } = buildChangesAndSchema(changesData)
839
+ if (!changes.length) return 'No valid changes found. Include at least one data section.'
840
+ const branch = args.branch || 'main'
841
+ const body = {
842
+ changes, granularChanges, orders,
843
+ message: args.message || 'Updated via Symbols MCP',
844
+ branch, type: 'patch'
845
+ }
846
+ const result = await apiRequest('POST', `/core/projects/${projectId}/changes`, { token: args.token, apiKey: args.api_key, body })
847
+ if (result.success) {
848
+ const d = result.data || {}
849
+ const version = d.value || d.version || d.id || 'unknown'
850
+ const savedSections = Object.keys(changesData)
851
+ return `Saved to project \`${args.project}\` successfully.\nVersion: ${version}\nBranch: ${branch}\nSections updated: ${savedSections.join(', ')}\n\nUse \`publish\` to make this version live, or \`push\` to deploy to an environment.`
852
+ }
853
+ return `Save failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
854
+ }
855
+
856
+ // publish
857
+ if (name === 'publish') {
858
+ const authErr = requireAuth(args.token, args.api_key)
859
+ if (authErr) return authErr
860
+ const { id: projectId, error: resolveErr } = await resolveProjectId(args.project, args.token, args.api_key)
861
+ if (resolveErr) return resolveErr
862
+ const body = { branch: args.branch || 'main' }
863
+ if (args.version) body.version = args.version
864
+ const result = await apiRequest('POST', `/core/projects/${projectId}/publish`, { token: args.token, apiKey: args.api_key, body })
865
+ if (result.success) {
866
+ const d = result.data || {}
867
+ return `Published successfully.\nVersion: ${d.value || d.id || 'unknown'}`
868
+ }
869
+ return `Publish failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
870
+ }
871
+
872
+ // push
873
+ if (name === 'push') {
874
+ const authErr = requireAuth(args.token, args.api_key)
875
+ if (authErr) return authErr
876
+ const { id: projectId, error: resolveErr } = await resolveProjectId(args.project, args.token, args.api_key)
877
+ if (resolveErr) return resolveErr
878
+ const environment = args.environment || 'production'
879
+ const mode = args.mode || 'published'
880
+ const branch = args.branch || 'main'
881
+ const body = { mode, branch }
882
+ if (args.version) body.version = args.version
883
+ const result = await apiRequest('POST', `/core/projects/${projectId}/environments/${environment}/publish`, { token: args.token, apiKey: args.api_key, body })
884
+ if (result.success) {
885
+ const d = result.data || {}
886
+ const config = d.config || {}
887
+ return `Pushed to ${d.key || environment} successfully.\nMode: ${config.mode || mode}\nVersion: ${config.version || 'latest'}\nBranch: ${config.branch || branch}`
888
+ }
889
+ return `Push failed: ${result.error || 'Unknown error'}\n${result.message || ''}`
890
+ }
891
+
892
+ throw new Error(`Unknown tool: ${name}`)
893
+ }
894
+
895
+ // ---------------------------------------------------------------------------
896
+ // JSON-RPC server
897
+ // ---------------------------------------------------------------------------
898
+
60
899
  function send(obj) {
61
900
  process.stdout.write(JSON.stringify(obj) + '\n')
62
901
  }
63
902
 
64
- function handle(req) {
903
+ async function handle(req) {
65
904
  if (!req.method) return
66
905
  if (req.method === 'initialize') {
67
906
  return send({
@@ -69,7 +908,7 @@ function handle(req) {
69
908
  result: {
70
909
  protocolVersion: req.params?.protocolVersion ?? '2025-03-26',
71
910
  capabilities: { tools: {} },
72
- serverInfo: { name: 'Symbols MCP', version: '1.0.6' }
911
+ serverInfo: { name: 'Symbols MCP', version: '1.0.15' }
73
912
  }
74
913
  })
75
914
  }
@@ -79,10 +918,7 @@ function handle(req) {
79
918
  if (req.method === 'tools/call') {
80
919
  const { name, arguments: args = {} } = req.params
81
920
  try {
82
- let text
83
- if (name === 'get_project_rules') text = loadAgentInstructions()
84
- else if (name === 'search_symbols_docs') text = searchDocs(args.query, args.max_results || 3)
85
- else throw new Error(`Unknown tool: ${name}`)
921
+ const text = await handleTool(name, args)
86
922
  return send({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text }] } })
87
923
  } catch (e) {
88
924
  return send({ jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: e.message }], isError: true } })
@@ -96,6 +932,7 @@ function handle(req) {
96
932
  const rl = readline.createInterface({ input: process.stdin, terminal: false })
97
933
  rl.on('line', line => {
98
934
  if (!line.trim()) return
99
- try { handle(JSON.parse(line)) } catch (e) { process.stderr.write(`Parse error: ${e.message}\n`) }
935
+ try { handle(JSON.parse(line)).catch(e => process.stderr.write(`Handler error: ${e.message}\n`)) }
936
+ catch (e) { process.stderr.write(`Parse error: ${e.message}\n`) }
100
937
  })
101
938
  rl.on('close', () => process.exit(0))