@symbo.ls/mcp-server 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Bundle skills from @symbo.ls/mcp into a single JS module.
3
+ *
4
+ * This pre-reads all .md files from the skills directory and writes them
5
+ * as a JS map so the Cloudflare Worker can use them without fs access.
6
+ *
7
+ * Run: node build.js
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { createRequire } from 'module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+
16
+ // Resolve skills directory from the @symbo.ls/mcp package
17
+ let skillsDir;
18
+ try {
19
+ const mcpPkg = require.resolve('@symbo.ls/mcp/package.json');
20
+ skillsDir = path.join(path.dirname(mcpPkg), 'symbols_mcp', 'skills');
21
+ } catch {
22
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
23
+ skillsDir = path.resolve(__dirname, '../../../symbols-mcp/symbols_mcp/skills');
24
+ }
25
+
26
+ if (!fs.existsSync(skillsDir)) {
27
+ console.error(`Skills directory not found: ${skillsDir}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ const files = fs.readdirSync(skillsDir).filter((f) => f.endsWith('.md'));
32
+ const skills = {};
33
+
34
+ for (const file of files) {
35
+ const content = fs.readFileSync(path.join(skillsDir, file), 'utf8');
36
+ skills[file] = content;
37
+ }
38
+
39
+ const output = `// Auto-generated by build.js — do not edit manually
40
+ // Source: ${skillsDir}
41
+ // Generated: ${new Date().toISOString()}
42
+
43
+ export const skills = ${JSON.stringify(skills, null, 2)};
44
+ `;
45
+
46
+ const outPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'src', 'skills-data.js');
47
+ fs.writeFileSync(outPath, output);
48
+
49
+ console.log(
50
+ `[BUILD] Bundled ${files.length} skills into src/skills-data.js (${output.length} bytes)`,
51
+ );
package/index.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Symbols MCP HTTP Proxy — Local Express Server
3
+ *
4
+ * Usage:
5
+ * node index.js # start on port 3335
6
+ * PORT=4000 node index.js # custom port
7
+ */
8
+
9
+ import express from 'express';
10
+ import cors from 'cors';
11
+ import {
12
+ handleJsonRpc,
13
+ TOOLS,
14
+ listResources,
15
+ readResource,
16
+ callTool,
17
+ PROMPTS,
18
+ } from './src/mcp-handler.js';
19
+
20
+ const PORT = parseInt(process.env.PORT, 10) || 3335;
21
+
22
+ const app = express();
23
+
24
+ app.use(cors({ origin: '*', methods: 'GET, POST' }));
25
+ app.use(express.json());
26
+
27
+ // Health check
28
+ app.get('/health', (req, res) => {
29
+ res.json({ alive: true, service: 'symbols-mcp', runtime: 'node' });
30
+ });
31
+
32
+ // MCP JSON-RPC endpoint
33
+ app.post('/mcp', (req, res) => {
34
+ const body = req.body;
35
+
36
+ // Batch requests
37
+ if (Array.isArray(body)) {
38
+ const results = body.map(handleJsonRpc).filter(Boolean);
39
+ return res.json(results);
40
+ }
41
+
42
+ const result = handleJsonRpc(body);
43
+ if (result === null) return res.status(204).end();
44
+ return res.json(result);
45
+ });
46
+
47
+ // REST: list tools
48
+ app.get('/tools', (req, res) => {
49
+ res.json({ tools: TOOLS });
50
+ });
51
+
52
+ // REST: call a tool
53
+ app.post('/tools/:name', (req, res) => {
54
+ try {
55
+ const result = callTool(req.params.name, req.body || {});
56
+ res.json({ content: [{ type: 'text', text: result }] });
57
+ } catch (e) {
58
+ res.status(404).json({ error: e.message });
59
+ }
60
+ });
61
+
62
+ // REST: list resources
63
+ app.get('/resources', (req, res) => {
64
+ res.json({ resources: listResources() });
65
+ });
66
+
67
+ // REST: read a resource
68
+ app.get('/resources/read', (req, res) => {
69
+ const uri = req.query.uri;
70
+ if (!uri) return res.status(400).json({ error: 'Missing ?uri= parameter' });
71
+ const content = readResource(uri);
72
+ if (content === null) return res.status(404).json({ error: `Resource not found: ${uri}` });
73
+ res.json({ contents: [{ uri, mimeType: 'text/markdown', text: content }] });
74
+ });
75
+
76
+ // REST: list prompts
77
+ app.get('/prompts', (req, res) => {
78
+ res.json({ prompts: PROMPTS });
79
+ });
80
+
81
+ app.listen(PORT, () => {
82
+ console.log(`[SYMBOLS-MCP] HTTP proxy running on http://localhost:${PORT}`);
83
+ console.log(`[SYMBOLS-MCP] MCP endpoint: POST http://localhost:${PORT}/mcp`);
84
+ console.log(`[SYMBOLS-MCP] REST API: GET /tools, /resources, /prompts`);
85
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@symbo.ls/mcp-server",
3
+ "version": "3.6.1",
4
+ "description": "HTTP proxy for the Symbols MCP server — runs as a Cloudflare Worker",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "build": "node build.js",
9
+ "prestart": "node build.js",
10
+ "start": "node index.js",
11
+ "dev": "node build.js && NODE_ENV=development node index.js",
12
+ "predeploy": "node build.js",
13
+ "deploy": "npx wrangler deploy",
14
+ "deploy:staging": "node build.js && npx wrangler deploy --env staging",
15
+ "deploy:production": "node build.js && npx wrangler deploy --env production",
16
+ "cf:dev": "node build.js && npx wrangler dev"
17
+ },
18
+ "dependencies": {
19
+ "@symbo.ls/mcp": "file:../../../symbols-mcp",
20
+ "express": "^4.18.2",
21
+ "cors": "^2.8.5"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ }
26
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Core MCP logic for the HTTP proxy.
3
+ *
4
+ * Uses pre-bundled skills data (generated by build.js) so it works
5
+ * in both Node.js and Cloudflare Worker environments without fs access.
6
+ */
7
+
8
+ import { skills } from './skills-data.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Skills access
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function readSkill(filename) {
15
+ return skills[filename] || `Skill '${filename}' not found`;
16
+ }
17
+
18
+ function listSkillFiles() {
19
+ return Object.keys(skills);
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Tools
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const TOOLS = [
27
+ {
28
+ name: 'get_project_rules',
29
+ description:
30
+ 'ALWAYS call this first before any generate_* tool. Returns the mandatory Symbols.app rules that MUST be followed.',
31
+ inputSchema: { type: 'object', properties: {} },
32
+ },
33
+ {
34
+ name: 'search_symbols_docs',
35
+ description: 'Search the Symbols documentation knowledge base for relevant information.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ query: {
40
+ type: 'string',
41
+ description: 'Natural language search query about Symbols/DOMQL',
42
+ },
43
+ max_results: {
44
+ type: 'number',
45
+ description: 'Maximum number of results (1-5)',
46
+ default: 3,
47
+ },
48
+ },
49
+ required: ['query'],
50
+ },
51
+ },
52
+ ];
53
+
54
+ function callTool(name, args = {}) {
55
+ if (name === 'get_project_rules') {
56
+ return readSkill('RULES.md');
57
+ }
58
+
59
+ if (name === 'search_symbols_docs') {
60
+ const query = args.query || '';
61
+ const maxResults = Math.min(Math.max(args.max_results || 3, 1), 5);
62
+ const keywords = query
63
+ .toLowerCase()
64
+ .split(/\s+/)
65
+ .filter((w) => w.length > 2);
66
+ if (!keywords.length) keywords.push(query.toLowerCase());
67
+
68
+ const results = [];
69
+ for (const fname of listSkillFiles()) {
70
+ const content = readSkill(fname);
71
+ const contentLower = content.toLowerCase();
72
+ if (!keywords.some((kw) => contentLower.includes(kw))) continue;
73
+
74
+ const lines = content.split('\n');
75
+ for (let i = 0; i < lines.length; i++) {
76
+ if (keywords.some((kw) => lines[i].toLowerCase().includes(kw))) {
77
+ results.push({
78
+ file: fname,
79
+ snippet: lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 20)).join('\n'),
80
+ });
81
+ break;
82
+ }
83
+ }
84
+ if (results.length >= maxResults) break;
85
+ }
86
+
87
+ return results.length
88
+ ? JSON.stringify(results, null, 2)
89
+ : `No results found for '${query}'. Try a different search term.`;
90
+ }
91
+
92
+ throw new Error(`Unknown tool: ${name}`);
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Resources — skill files exposed as MCP resources
97
+ // ---------------------------------------------------------------------------
98
+
99
+ const SKILL_RESOURCES = {
100
+ 'symbols://skills/rules': 'RULES.md',
101
+ 'symbols://skills/syntax': 'SYNTAX.md',
102
+ 'symbols://skills/components': 'COMPONENTS.md',
103
+ 'symbols://skills/project-structure': 'PROJECT_STRUCTURE.md',
104
+ 'symbols://skills/design-system': 'DESIGN_SYSTEM.md',
105
+ 'symbols://skills/design-direction': 'DESIGN_DIRECTION.md',
106
+ 'symbols://skills/patterns': 'PATTERNS.md',
107
+ 'symbols://skills/migration': 'MIGRATION.md',
108
+ 'symbols://skills/audit': 'AUDIT.md',
109
+ 'symbols://skills/design-to-code': 'DESIGN_TO_CODE.md',
110
+ 'symbols://skills/seo-metadata': 'SEO-METADATA.md',
111
+ };
112
+
113
+ function listResources() {
114
+ const resources = [];
115
+ for (const [uri, filename] of Object.entries(SKILL_RESOURCES)) {
116
+ if (skills[filename]) {
117
+ resources.push({ uri, name: filename.replace('.md', ''), mimeType: 'text/markdown' });
118
+ }
119
+ }
120
+ return resources;
121
+ }
122
+
123
+ function readResource(uri) {
124
+ const filename = SKILL_RESOURCES[uri];
125
+ if (filename && skills[filename]) {
126
+ return readSkill(filename);
127
+ }
128
+ return null;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Prompts
133
+ // ---------------------------------------------------------------------------
134
+
135
+ const PROMPTS = [
136
+ {
137
+ name: 'symbols_component_prompt',
138
+ description: 'Prompt template for generating a Symbols.app component.',
139
+ arguments: [
140
+ { name: 'description', description: 'Component description', required: true },
141
+ { name: 'component_name', description: 'Component name', required: false },
142
+ ],
143
+ },
144
+ {
145
+ name: 'symbols_migration_prompt',
146
+ description: 'Prompt template for migrating code to Symbols.app.',
147
+ arguments: [
148
+ {
149
+ name: 'source_framework',
150
+ description: 'Source framework (React, Vue, etc.)',
151
+ required: false,
152
+ },
153
+ ],
154
+ },
155
+ {
156
+ name: 'symbols_project_prompt',
157
+ description: 'Prompt template for scaffolding a complete Symbols project.',
158
+ arguments: [{ name: 'description', description: 'Project description', required: true }],
159
+ },
160
+ {
161
+ name: 'symbols_review_prompt',
162
+ description: 'Prompt template for reviewing Symbols/DOMQL code.',
163
+ arguments: [],
164
+ },
165
+ ];
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // MCP JSON-RPC handler
169
+ // ---------------------------------------------------------------------------
170
+
171
+ const SERVER_INFO = { name: 'Symbols MCP', version: '1.0.10' };
172
+
173
+ export function handleJsonRpc(req) {
174
+ const { method, params, id } = req;
175
+
176
+ if (method === 'initialize') {
177
+ return {
178
+ jsonrpc: '2.0',
179
+ id,
180
+ result: {
181
+ protocolVersion: params?.protocolVersion ?? '2025-03-26',
182
+ capabilities: { tools: {}, resources: {}, prompts: {} },
183
+ serverInfo: SERVER_INFO,
184
+ },
185
+ };
186
+ }
187
+
188
+ if (method === 'notifications/initialized') return null;
189
+ if (method === 'ping') return { jsonrpc: '2.0', id, result: {} };
190
+
191
+ if (method === 'tools/list') {
192
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
193
+ }
194
+
195
+ if (method === 'tools/call') {
196
+ const { name, arguments: args = {} } = params || {};
197
+ try {
198
+ const text = callTool(name, args);
199
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } };
200
+ } catch (e) {
201
+ return {
202
+ jsonrpc: '2.0',
203
+ id,
204
+ result: { content: [{ type: 'text', text: e.message }], isError: true },
205
+ };
206
+ }
207
+ }
208
+
209
+ if (method === 'resources/list') {
210
+ return { jsonrpc: '2.0', id, result: { resources: listResources() } };
211
+ }
212
+
213
+ if (method === 'resources/read') {
214
+ const uri = params?.uri;
215
+ const content = readResource(uri);
216
+ if (content === null) {
217
+ return { jsonrpc: '2.0', id, error: { code: -32602, message: `Resource not found: ${uri}` } };
218
+ }
219
+ return {
220
+ jsonrpc: '2.0',
221
+ id,
222
+ result: { contents: [{ uri, mimeType: 'text/markdown', text: content }] },
223
+ };
224
+ }
225
+
226
+ if (method === 'prompts/list') {
227
+ return { jsonrpc: '2.0', id, result: { prompts: PROMPTS } };
228
+ }
229
+
230
+ if (id !== undefined) {
231
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } };
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ export { TOOLS, listResources, readResource, callTool, PROMPTS };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Cloudflare Worker request handler for Symbols MCP HTTP proxy.
3
+ *
4
+ * Exposes the MCP server over HTTP with two interfaces:
5
+ * 1. MCP JSON-RPC protocol (POST /mcp)
6
+ * 2. REST-style endpoints for direct access
7
+ */
8
+
9
+ import {
10
+ handleJsonRpc,
11
+ TOOLS,
12
+ listResources,
13
+ readResource,
14
+ callTool,
15
+ PROMPTS,
16
+ } from './mcp-handler.js';
17
+
18
+ const CORS_HEADERS = {
19
+ 'Access-Control-Allow-Origin': '*',
20
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
21
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
22
+ };
23
+
24
+ function json(data, status = 200) {
25
+ return new Response(JSON.stringify(data), {
26
+ status,
27
+ headers: { 'Content-Type': 'application/json', ...CORS_HEADERS },
28
+ });
29
+ }
30
+
31
+ export async function handleRequest(request) {
32
+ const url = new URL(request.url);
33
+ const { pathname } = url;
34
+
35
+ // CORS preflight
36
+ if (request.method === 'OPTIONS') {
37
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
38
+ }
39
+
40
+ // ── MCP JSON-RPC endpoint ──────────────────────────────────────────────
41
+ if (pathname === '/mcp' && request.method === 'POST') {
42
+ let body;
43
+ try {
44
+ body = await request.json();
45
+ } catch {
46
+ return json({ error: 'Invalid JSON body' }, 400);
47
+ }
48
+
49
+ // Support batch requests
50
+ if (Array.isArray(body)) {
51
+ const results = body.map(handleJsonRpc).filter(Boolean);
52
+ return json(results);
53
+ }
54
+
55
+ const result = handleJsonRpc(body);
56
+ if (result === null) return new Response(null, { status: 204 });
57
+ return json(result);
58
+ }
59
+
60
+ // ── REST endpoints ─────────────────────────────────────────────────────
61
+
62
+ // GET /tools — list all tools
63
+ if (pathname === '/tools' && request.method === 'GET') {
64
+ return json({ tools: TOOLS });
65
+ }
66
+
67
+ // POST /tools/:name — call a tool
68
+ if (pathname.startsWith('/tools/') && request.method === 'POST') {
69
+ const toolName = pathname.slice('/tools/'.length);
70
+ let args = {};
71
+ try {
72
+ const text = await request.text();
73
+ if (text) args = JSON.parse(text);
74
+ } catch {
75
+ return json({ error: 'Invalid JSON body' }, 400);
76
+ }
77
+
78
+ try {
79
+ const result = callTool(toolName, args);
80
+ return json({ content: [{ type: 'text', text: result }] });
81
+ } catch (e) {
82
+ return json({ error: e.message }, 404);
83
+ }
84
+ }
85
+
86
+ // GET /resources — list all resources
87
+ if (pathname === '/resources' && request.method === 'GET') {
88
+ return json({ resources: listResources() });
89
+ }
90
+
91
+ // GET /resources/:uri — read a resource (uri passed as query param)
92
+ if (pathname === '/resources/read' && request.method === 'GET') {
93
+ const uri = url.searchParams.get('uri');
94
+ if (!uri) return json({ error: 'Missing ?uri= parameter' }, 400);
95
+ const content = readResource(uri);
96
+ if (content === null) return json({ error: `Resource not found: ${uri}` }, 404);
97
+ return json({ contents: [{ uri, mimeType: 'text/markdown', text: content }] });
98
+ }
99
+
100
+ // GET /prompts — list all prompts
101
+ if (pathname === '/prompts' && request.method === 'GET') {
102
+ return json({ prompts: PROMPTS });
103
+ }
104
+
105
+ return json({ error: 'Not found' }, 404);
106
+ }
package/worker.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Cloudflare Worker entry point for Symbols MCP HTTP proxy.
3
+ *
4
+ * Deploy:
5
+ * npx wrangler deploy # dev (default)
6
+ * npx wrangler deploy --env staging
7
+ * npx wrangler deploy --env production
8
+ *
9
+ * Dev:
10
+ * npx wrangler dev
11
+ */
12
+
13
+ export default {
14
+ async fetch(request, env) {
15
+ const url = new URL(request.url);
16
+
17
+ // Health check
18
+ if (url.pathname === '/health') {
19
+ return Response.json({
20
+ alive: true,
21
+ service: 'symbols-mcp',
22
+ runtime: 'cloudflare-worker',
23
+ });
24
+ }
25
+
26
+ try {
27
+ if (env.NODE_ENV) process.env.NODE_ENV = env.NODE_ENV;
28
+ if (env.LOG_LEVEL) process.env.LOG_LEVEL = env.LOG_LEVEL;
29
+
30
+ const { handleRequest } = await import('./src/worker-handler.js');
31
+ return await handleRequest(request);
32
+ } catch (err) {
33
+ return new Response(`Internal Server Error: ${err.message}`, { status: 500 });
34
+ }
35
+ },
36
+ };
package/wrangler.toml ADDED
@@ -0,0 +1,50 @@
1
+ # Cloudflare Workers configuration for Symbols MCP HTTP proxy
2
+ #
3
+ # Deploy:
4
+ # npx wrangler deploy # dev (default)
5
+ # npx wrangler deploy --env staging
6
+ # npx wrangler deploy --env production
7
+ #
8
+ # Local dev:
9
+ # npx wrangler dev
10
+
11
+ name = "symbols-mcp-dev"
12
+ main = "worker.js"
13
+ compatibility_date = "2024-09-01"
14
+ compatibility_flags = ["nodejs_compat_v2"]
15
+ node_compat = true
16
+
17
+ # Default vars (dev)
18
+ [vars]
19
+ NODE_ENV = "development"
20
+ LOG_LEVEL = "debug"
21
+
22
+ # ── Staging ──────────────────────────────────────────────────────────────────
23
+
24
+ [env.staging]
25
+ name = "symbols-mcp-staging"
26
+
27
+ [env.staging.vars]
28
+ NODE_ENV = "staging"
29
+ LOG_LEVEL = "info"
30
+
31
+ # ── Production ───────────────────────────────────────────────────────────────
32
+
33
+ [env.production]
34
+ name = "symbols-mcp"
35
+
36
+ [env.production.vars]
37
+ NODE_ENV = "production"
38
+ LOG_LEVEL = "info"
39
+
40
+ # ── Routes (configure once DNS is ready) ─────────────────────────────────────
41
+ #
42
+ # Production: mcp.symbols.app
43
+ # [env.production.routes]
44
+ # pattern = "mcp.symbols.app/*"
45
+ # zone_name = "symbols.app"
46
+ #
47
+ # Dev: mcp.dev.symbols.app
48
+ # [[routes]]
49
+ # pattern = "mcp.dev.symbols.app/*"
50
+ # zone_name = "symbols.app"