@symbo.ls/mcp-server 3.6.6 → 3.6.8
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/package.json +1 -1
- package/src/mcp-handler.js +214 -1
- package/src/worker-handler.js +117 -30
- package/worker.js +0 -12
- package/wrangler.toml +6 -13
package/package.json
CHANGED
package/src/mcp-handler.js
CHANGED
|
@@ -15,10 +15,70 @@ function readSkill(filename) {
|
|
|
15
15
|
return skills[filename] || `Skill '${filename}' not found`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function readSkills(...filenames) {
|
|
19
|
+
return filenames
|
|
20
|
+
.map((f) => skills[f])
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join('\n\n---\n\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
function listSkillFiles() {
|
|
19
26
|
return Object.keys(skills);
|
|
20
27
|
}
|
|
21
28
|
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Audit helpers (deterministic rule checking)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const V2_PATTERNS = [
|
|
34
|
+
[/\bextend\s*:/g, "v2 syntax: use 'extends' (plural) instead of 'extend'"],
|
|
35
|
+
[/\bchildExtend\s*:/g, "v2 syntax: use 'childExtends' (plural) instead of 'childExtend'"],
|
|
36
|
+
[/\bon\s*:\s*\{/g, 'v2 syntax: flatten event handlers with onX prefix (e.g. onClick) instead of on: {} wrapper'],
|
|
37
|
+
[/\bprops\s*:\s*\{(?!\s*\})/g, 'v2 syntax: flatten props directly on the component instead of props: {} wrapper'],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const RULE_CHECKS = [
|
|
41
|
+
[/\bimport\s+.*\bfrom\s+['"]\.\//, 'FORBIDDEN: No imports between project files — reference components by PascalCase key name'],
|
|
42
|
+
[/\bexport\s+default\s+\{/, 'Components should use named exports (export const Name = {}), not default exports'],
|
|
43
|
+
[/\bfunction\s+\w+\s*\(.*\)\s*\{[\s\S]*?return\s*\{/, 'Components must be plain objects, not functions that return objects'],
|
|
44
|
+
[/(?:padding|margin|gap|width|height)\s*:\s*['"]?\d+px/, 'Use design tokens (A, B, C) instead of hardcoded pixel values'],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function auditCode(code) {
|
|
48
|
+
const violations = [];
|
|
49
|
+
const warnings = [];
|
|
50
|
+
|
|
51
|
+
for (const [pattern, message] of V2_PATTERNS) {
|
|
52
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = regex.exec(code)) !== null) {
|
|
55
|
+
const line = code.slice(0, match.index).split('\n').length;
|
|
56
|
+
violations.push({ line, severity: 'error', message });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [pattern, message] of RULE_CHECKS) {
|
|
61
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = regex.exec(code)) !== null) {
|
|
64
|
+
const line = code.slice(0, match.index).split('\n').length;
|
|
65
|
+
const level = message.includes('FORBIDDEN') ? 'error' : 'warning';
|
|
66
|
+
(level === 'error' ? violations : warnings).push({ line, severity: level, message });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const totalIssues = violations.length + warnings.length;
|
|
71
|
+
const score = Math.max(1, 10 - totalIssues);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
passed: violations.length === 0,
|
|
75
|
+
score,
|
|
76
|
+
violations,
|
|
77
|
+
warnings,
|
|
78
|
+
summary: `${violations.length} errors, ${warnings.length} warnings — compliance score: ${score}/10`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
22
82
|
// ---------------------------------------------------------------------------
|
|
23
83
|
// Tools
|
|
24
84
|
// ---------------------------------------------------------------------------
|
|
@@ -49,6 +109,91 @@ const TOOLS = [
|
|
|
49
109
|
required: ['query'],
|
|
50
110
|
},
|
|
51
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: 'generate_component',
|
|
114
|
+
description:
|
|
115
|
+
'Generate a Symbols.app DOMQL v3 component from a description. Returns rules, syntax, component catalog, cookbook examples, and default library reference as context.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
description: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'What the component should do and look like',
|
|
122
|
+
},
|
|
123
|
+
component_name: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'PascalCase name for the component',
|
|
126
|
+
default: 'MyComponent',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ['description'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'generate_page',
|
|
134
|
+
description:
|
|
135
|
+
'Generate a Symbols.app page with routing integration. Returns rules, project structure, patterns, snippets, and default library reference as context.',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
description: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
description: 'What the page should contain and do',
|
|
142
|
+
},
|
|
143
|
+
page_name: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'camelCase name for the page',
|
|
146
|
+
default: 'home',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
required: ['description'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'convert_react',
|
|
154
|
+
description:
|
|
155
|
+
'Convert React/JSX code to Symbols.app DOMQL v3. Returns migration rules, syntax reference, and examples as context.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
source_code: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
description: 'The React/JSX source code to convert',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
required: ['source_code'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'convert_html',
|
|
169
|
+
description:
|
|
170
|
+
'Convert raw HTML/CSS to Symbols.app DOMQL v3 components. Returns component catalog, syntax reference, and design tokens as context.',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
source_code: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'The HTML/CSS source code to convert',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: ['source_code'],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'audit_component',
|
|
184
|
+
description:
|
|
185
|
+
'Audit a Symbols/DOMQL component for v3 compliance and best practices. Returns a structured report with violations, warnings, and compliance score.',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
component_code: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: 'The JavaScript component code to audit',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
required: ['component_code'],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
52
197
|
];
|
|
53
198
|
|
|
54
199
|
function callTool(name, args = {}) {
|
|
@@ -89,6 +234,59 @@ function callTool(name, args = {}) {
|
|
|
89
234
|
: `No results found for '${query}'. Try a different search term.`;
|
|
90
235
|
}
|
|
91
236
|
|
|
237
|
+
if (name === 'generate_component') {
|
|
238
|
+
const desc = args.description || '';
|
|
239
|
+
const compName = args.component_name || 'MyComponent';
|
|
240
|
+
const context = readSkills('RULES.md', 'COMPONENTS.md', 'SYNTAX.md', 'COOKBOOK.md', 'DEFAULT_LIBRARY.md');
|
|
241
|
+
return `# Generate Component: ${compName}\n\n## Description\n${desc}\n\n## Requirements\n- Named export: \`export const ${compName} = { ... }\`\n- DOMQL v3 syntax only (extends, childExtends, flattened props, onX events)\n- Use design tokens for spacing (A, B, C), colors from theme\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- Follow modern UI/UX: visual hierarchy, confident typography, minimal cognitive load\n\n## Context — Rules, Syntax & Examples\n\n${context}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (name === 'generate_page') {
|
|
245
|
+
const desc = args.description || '';
|
|
246
|
+
const pageName = args.page_name || 'home';
|
|
247
|
+
const context = readSkills('RULES.md', 'PROJECT_STRUCTURE.md', 'PATTERNS.md', 'SNIPPETS.md', 'DEFAULT_LIBRARY.md', 'COMPONENTS.md');
|
|
248
|
+
return `# Generate Page: ${pageName}\n\n## Description\n${desc}\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- Use design tokens — no hardcoded pixels\n- Include responsive layout adjustments\n\n## Context — Rules, Structure, Patterns & Snippets\n\n${context}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (name === 'convert_react') {
|
|
252
|
+
const source = args.source_code || '';
|
|
253
|
+
const context = readSkills('RULES.md', 'MIGRATION.md', 'SYNTAX.md', 'COMPONENTS.md', 'LEARNINGS.md');
|
|
254
|
+
return `# Convert React → Symbols DOMQL v3\n\n## Source Code to Convert\n\`\`\`jsx\n${source}\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}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (name === 'convert_html') {
|
|
258
|
+
const source = args.source_code || '';
|
|
259
|
+
const context = readSkills('RULES.md', 'SYNTAX.md', 'COMPONENTS.md', 'DESIGN_SYSTEM.md', 'SNIPPETS.md', 'LEARNINGS.md');
|
|
260
|
+
return `# Convert HTML → Symbols DOMQL v3\n\n## Source Code to Convert\n\`\`\`html\n${source}\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}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (name === 'audit_component') {
|
|
264
|
+
const code = args.component_code || '';
|
|
265
|
+
const result = auditCode(code);
|
|
266
|
+
const rulesContext = readSkill('AUDIT.md');
|
|
267
|
+
|
|
268
|
+
let output = `# Audit Report\n\n## Summary\n${result.summary}\nPassed: ${result.passed ? 'Yes' : 'No'}\n\n## Violations (Errors)\n`;
|
|
269
|
+
if (result.violations.length) {
|
|
270
|
+
for (const v of result.violations) {
|
|
271
|
+
output += `- **Line ${v.line}**: ${v.message}\n`;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
output += 'No violations found.\n';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
output += '\n## Warnings\n';
|
|
278
|
+
if (result.warnings.length) {
|
|
279
|
+
for (const w of result.warnings) {
|
|
280
|
+
output += `- **Line ${w.line}**: ${w.message}\n`;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
output += 'No warnings.\n';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
output += `\n## Detailed Rules Reference\n\n${rulesContext}`;
|
|
287
|
+
return output;
|
|
288
|
+
}
|
|
289
|
+
|
|
92
290
|
throw new Error(`Unknown tool: ${name}`);
|
|
93
291
|
}
|
|
94
292
|
|
|
@@ -108,6 +306,11 @@ const SKILL_RESOURCES = {
|
|
|
108
306
|
'symbols://skills/audit': 'AUDIT.md',
|
|
109
307
|
'symbols://skills/design-to-code': 'DESIGN_TO_CODE.md',
|
|
110
308
|
'symbols://skills/seo-metadata': 'SEO-METADATA.md',
|
|
309
|
+
'symbols://skills/ssr-brender': 'SSR-BRENDER.md',
|
|
310
|
+
'symbols://skills/cookbook': 'COOKBOOK.md',
|
|
311
|
+
'symbols://skills/snippets': 'SNIPPETS.md',
|
|
312
|
+
'symbols://skills/default-library': 'DEFAULT_LIBRARY.md',
|
|
313
|
+
'symbols://skills/learnings': 'LEARNINGS.md',
|
|
111
314
|
};
|
|
112
315
|
|
|
113
316
|
function listResources() {
|
|
@@ -162,13 +365,23 @@ const PROMPTS = [
|
|
|
162
365
|
description: 'Prompt template for reviewing Symbols/DOMQL code.',
|
|
163
366
|
arguments: [],
|
|
164
367
|
},
|
|
368
|
+
{
|
|
369
|
+
name: 'symbols_convert_html_prompt',
|
|
370
|
+
description: 'Prompt template for converting HTML to Symbols.app components.',
|
|
371
|
+
arguments: [],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'symbols_design_review_prompt',
|
|
375
|
+
description: 'Prompt template for visual/design audit against the design system.',
|
|
376
|
+
arguments: [],
|
|
377
|
+
},
|
|
165
378
|
];
|
|
166
379
|
|
|
167
380
|
// ---------------------------------------------------------------------------
|
|
168
381
|
// MCP JSON-RPC handler
|
|
169
382
|
// ---------------------------------------------------------------------------
|
|
170
383
|
|
|
171
|
-
const SERVER_INFO = { name: 'Symbols MCP', version: '1.0
|
|
384
|
+
const SERVER_INFO = { name: 'Symbols MCP', version: '1.1.0' };
|
|
172
385
|
|
|
173
386
|
export function handleJsonRpc(req) {
|
|
174
387
|
const { method, params, id } = req;
|
package/src/worker-handler.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloudflare Worker request handler for Symbols MCP HTTP
|
|
2
|
+
* Cloudflare Worker request handler for Symbols MCP — Streamable HTTP transport.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* MCP endpoint is root (/):
|
|
5
|
+
* - POST / — JSON-RPC requests/notifications/responses
|
|
6
|
+
* - GET / — SSE stream (returns 405, not needed for stateless server)
|
|
7
|
+
* - DELETE / — terminate session (returns 405)
|
|
8
|
+
*
|
|
9
|
+
* Also exposes REST endpoints for direct access.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import {
|
|
@@ -17,15 +20,77 @@ import {
|
|
|
17
20
|
|
|
18
21
|
const CORS_HEADERS = {
|
|
19
22
|
'Access-Control-Allow-Origin': '*',
|
|
20
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
21
|
-
'Access-Control-Allow-Headers':
|
|
23
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
24
|
+
'Access-Control-Allow-Headers':
|
|
25
|
+
'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version',
|
|
26
|
+
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
function json(data, status = 200) {
|
|
29
|
+
function json(data, status = 200, extraHeaders = {}) {
|
|
25
30
|
return new Response(JSON.stringify(data), {
|
|
26
31
|
status,
|
|
27
|
-
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS },
|
|
32
|
+
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS, ...extraHeaders },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sseResponse(data, extraHeaders = {}) {
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
const stream = new ReadableStream({
|
|
39
|
+
start(controller) {
|
|
40
|
+
controller.enqueue(encoder.encode(`event: message\ndata: ${JSON.stringify(data)}\n\n`));
|
|
41
|
+
controller.close();
|
|
42
|
+
},
|
|
28
43
|
});
|
|
44
|
+
|
|
45
|
+
return new Response(stream, {
|
|
46
|
+
status: 200,
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'text/event-stream',
|
|
49
|
+
'Cache-Control': 'no-cache',
|
|
50
|
+
'Connection': 'keep-alive',
|
|
51
|
+
...CORS_HEADERS,
|
|
52
|
+
...extraHeaders,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function generateSessionId() {
|
|
58
|
+
const bytes = new Uint8Array(16);
|
|
59
|
+
crypto.getRandomValues(bytes);
|
|
60
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isNotificationOrResponse(msg) {
|
|
64
|
+
if (msg.method && msg.id === undefined) return true;
|
|
65
|
+
if (!msg.method && (msg.result !== undefined || msg.error !== undefined)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleMcpPost(request, body) {
|
|
70
|
+
// Notifications and responses get 202 Accepted
|
|
71
|
+
if (isNotificationOrResponse(body)) {
|
|
72
|
+
return new Response(null, { status: 202, headers: CORS_HEADERS });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Handle JSON-RPC request
|
|
76
|
+
const result = handleJsonRpc(body);
|
|
77
|
+
if (result === null) {
|
|
78
|
+
return new Response(null, { status: 202, headers: CORS_HEADERS });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// For initialize, generate and attach session ID
|
|
82
|
+
const extraHeaders = {};
|
|
83
|
+
if (body.method === 'initialize') {
|
|
84
|
+
extraHeaders['Mcp-Session-Id'] = generateSessionId();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if client accepts SSE
|
|
88
|
+
const accept = request.headers.get('Accept') || '';
|
|
89
|
+
if (accept.includes('text/event-stream')) {
|
|
90
|
+
return sseResponse(result, extraHeaders);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return json(result, 200, extraHeaders);
|
|
29
94
|
}
|
|
30
95
|
|
|
31
96
|
export async function handleRequest(request) {
|
|
@@ -37,34 +102,60 @@ export async function handleRequest(request) {
|
|
|
37
102
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
38
103
|
}
|
|
39
104
|
|
|
40
|
-
//
|
|
41
|
-
if (pathname === '/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
105
|
+
// OAuth metadata discovery — no auth required, return 404
|
|
106
|
+
if (pathname === '/.well-known/oauth-authorization-server') {
|
|
107
|
+
return new Response('Not Found', { status: 404 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── MCP Streamable HTTP transport on root (/) and /mcp ─────────────
|
|
111
|
+
|
|
112
|
+
if (pathname === '/' || pathname === '' || pathname === '/mcp') {
|
|
113
|
+
if (request.method === 'GET') {
|
|
114
|
+
const accept = request.headers.get('Accept') || '';
|
|
115
|
+
if (accept.includes('text/event-stream')) {
|
|
116
|
+
return new Response('Method Not Allowed', { status: 405, headers: CORS_HEADERS });
|
|
117
|
+
}
|
|
118
|
+
return json({
|
|
119
|
+
name: 'Symbols MCP',
|
|
120
|
+
version: '1.0.11',
|
|
121
|
+
description: 'MCP server for Symbols.app — documentation search and framework reference',
|
|
122
|
+
endpoints: {
|
|
123
|
+
mcp: 'POST / (Streamable HTTP transport)',
|
|
124
|
+
health: 'GET /health',
|
|
125
|
+
tools: 'GET /tools',
|
|
126
|
+
resources: 'GET /resources',
|
|
127
|
+
prompts: 'GET /prompts',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
47
130
|
}
|
|
48
131
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const results = body.map(handleJsonRpc).filter(Boolean);
|
|
52
|
-
return json(results);
|
|
132
|
+
if (request.method === 'DELETE') {
|
|
133
|
+
return new Response(null, { status: 405, headers: CORS_HEADERS });
|
|
53
134
|
}
|
|
54
135
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
136
|
+
if (request.method === 'POST') {
|
|
137
|
+
let body;
|
|
138
|
+
try {
|
|
139
|
+
body = await request.json();
|
|
140
|
+
} catch {
|
|
141
|
+
return json({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' } }, 400);
|
|
142
|
+
}
|
|
143
|
+
return handleMcpPost(request, body);
|
|
144
|
+
}
|
|
58
145
|
}
|
|
59
146
|
|
|
60
|
-
// ──
|
|
147
|
+
// ── Health check ────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
if (pathname === '/health' && request.method === 'GET') {
|
|
150
|
+
return json({ alive: true, service: 'symbols-mcp', runtime: 'cloudflare-worker' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── REST endpoints ─────────────────────────────────────────────────
|
|
61
154
|
|
|
62
|
-
// GET /tools — list all tools
|
|
63
155
|
if (pathname === '/tools' && request.method === 'GET') {
|
|
64
156
|
return json({ tools: TOOLS });
|
|
65
157
|
}
|
|
66
158
|
|
|
67
|
-
// POST /tools/:name — call a tool
|
|
68
159
|
if (pathname.startsWith('/tools/') && request.method === 'POST') {
|
|
69
160
|
const toolName = pathname.slice('/tools/'.length);
|
|
70
161
|
let args = {};
|
|
@@ -74,7 +165,6 @@ export async function handleRequest(request) {
|
|
|
74
165
|
} catch {
|
|
75
166
|
return json({ error: 'Invalid JSON body' }, 400);
|
|
76
167
|
}
|
|
77
|
-
|
|
78
168
|
try {
|
|
79
169
|
const result = callTool(toolName, args);
|
|
80
170
|
return json({ content: [{ type: 'text', text: result }] });
|
|
@@ -83,12 +173,10 @@ export async function handleRequest(request) {
|
|
|
83
173
|
}
|
|
84
174
|
}
|
|
85
175
|
|
|
86
|
-
// GET /resources — list all resources
|
|
87
176
|
if (pathname === '/resources' && request.method === 'GET') {
|
|
88
177
|
return json({ resources: listResources() });
|
|
89
178
|
}
|
|
90
179
|
|
|
91
|
-
// GET /resources/:uri — read a resource (uri passed as query param)
|
|
92
180
|
if (pathname === '/resources/read' && request.method === 'GET') {
|
|
93
181
|
const uri = url.searchParams.get('uri');
|
|
94
182
|
if (!uri) return json({ error: 'Missing ?uri= parameter' }, 400);
|
|
@@ -97,10 +185,9 @@ export async function handleRequest(request) {
|
|
|
97
185
|
return json({ contents: [{ uri, mimeType: 'text/markdown', text: content }] });
|
|
98
186
|
}
|
|
99
187
|
|
|
100
|
-
// GET /prompts — list all prompts
|
|
101
188
|
if (pathname === '/prompts' && request.method === 'GET') {
|
|
102
189
|
return json({ prompts: PROMPTS });
|
|
103
190
|
}
|
|
104
191
|
|
|
105
|
-
return
|
|
192
|
+
return new Response('Not Found', { status: 404 });
|
|
106
193
|
}
|
package/worker.js
CHANGED
|
@@ -14,19 +14,7 @@ export default {
|
|
|
14
14
|
async fetch(request, env) {
|
|
15
15
|
const url = new URL(request.url);
|
|
16
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
17
|
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
18
|
const { handleRequest } = await import('./src/worker-handler.js');
|
|
31
19
|
return await handleRequest(request);
|
|
32
20
|
} catch (err) {
|
package/wrangler.toml
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
# Local dev:
|
|
9
9
|
# npx wrangler dev
|
|
10
10
|
|
|
11
|
+
account_id = "98091ed46ea209bb648b108950bd78e7"
|
|
11
12
|
name = "symbols-mcp-dev"
|
|
12
13
|
main = "worker.js"
|
|
13
14
|
compatibility_date = "2024-09-01"
|
|
14
|
-
compatibility_flags = ["
|
|
15
|
-
node_compat = true
|
|
15
|
+
compatibility_flags = ["nodejs_compat"]
|
|
16
16
|
|
|
17
17
|
# Default vars (dev)
|
|
18
18
|
[vars]
|
|
@@ -32,19 +32,12 @@ LOG_LEVEL = "info"
|
|
|
32
32
|
|
|
33
33
|
[env.production]
|
|
34
34
|
name = "symbols-mcp"
|
|
35
|
+
workers_dev = true
|
|
35
36
|
|
|
36
37
|
[env.production.vars]
|
|
37
38
|
NODE_ENV = "production"
|
|
38
39
|
LOG_LEVEL = "info"
|
|
39
40
|
|
|
40
|
-
# ── Routes
|
|
41
|
-
|
|
42
|
-
#
|
|
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"
|
|
41
|
+
# ── Routes ────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
# Route handled via Cloudflare DNS CNAME → workers.dev
|