@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 +51 -0
- package/index.js +85 -0
- package/package.json +26 -0
- package/src/mcp-handler.js +237 -0
- package/src/worker-handler.js +106 -0
- package/worker.js +36 -0
- package/wrangler.toml +50 -0
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"
|