@symbo.ls/mcp-server 3.6.6 → 3.6.7
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/worker-handler.js +116 -30
- package/worker.js +0 -12
- package/wrangler.toml +6 -13
package/package.json
CHANGED
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,76 @@ 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': 'Content-Type, Authorization',
|
|
23
|
+
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
24
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version',
|
|
25
|
+
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
function json(data, status = 200) {
|
|
28
|
+
function json(data, status = 200, extraHeaders = {}) {
|
|
25
29
|
return new Response(JSON.stringify(data), {
|
|
26
30
|
status,
|
|
27
|
-
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS },
|
|
31
|
+
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS, ...extraHeaders },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sseResponse(data, extraHeaders = {}) {
|
|
36
|
+
const encoder = new TextEncoder();
|
|
37
|
+
const stream = new ReadableStream({
|
|
38
|
+
start(controller) {
|
|
39
|
+
controller.enqueue(encoder.encode(`event: message\ndata: ${JSON.stringify(data)}\n\n`));
|
|
40
|
+
controller.close();
|
|
41
|
+
},
|
|
28
42
|
});
|
|
43
|
+
|
|
44
|
+
return new Response(stream, {
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'text/event-stream',
|
|
48
|
+
'Cache-Control': 'no-cache',
|
|
49
|
+
'Connection': 'keep-alive',
|
|
50
|
+
...CORS_HEADERS,
|
|
51
|
+
...extraHeaders,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function generateSessionId() {
|
|
57
|
+
const bytes = new Uint8Array(16);
|
|
58
|
+
crypto.getRandomValues(bytes);
|
|
59
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isNotificationOrResponse(msg) {
|
|
63
|
+
if (msg.method && msg.id === undefined) return true;
|
|
64
|
+
if (!msg.method && (msg.result !== undefined || msg.error !== undefined)) return true;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleMcpPost(request, body) {
|
|
69
|
+
// Notifications and responses get 202 Accepted
|
|
70
|
+
if (isNotificationOrResponse(body)) {
|
|
71
|
+
return new Response(null, { status: 202, headers: CORS_HEADERS });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle JSON-RPC request
|
|
75
|
+
const result = handleJsonRpc(body);
|
|
76
|
+
if (result === null) {
|
|
77
|
+
return new Response(null, { status: 202, headers: CORS_HEADERS });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For initialize, generate and attach session ID
|
|
81
|
+
const extraHeaders = {};
|
|
82
|
+
if (body.method === 'initialize') {
|
|
83
|
+
extraHeaders['Mcp-Session-Id'] = generateSessionId();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if client accepts SSE
|
|
87
|
+
const accept = request.headers.get('Accept') || '';
|
|
88
|
+
if (accept.includes('text/event-stream')) {
|
|
89
|
+
return sseResponse(result, extraHeaders);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return json(result, 200, extraHeaders);
|
|
29
93
|
}
|
|
30
94
|
|
|
31
95
|
export async function handleRequest(request) {
|
|
@@ -37,34 +101,60 @@ export async function handleRequest(request) {
|
|
|
37
101
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
38
102
|
}
|
|
39
103
|
|
|
40
|
-
//
|
|
41
|
-
if (pathname === '/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
104
|
+
// OAuth metadata discovery — no auth required, return 404
|
|
105
|
+
if (pathname === '/.well-known/oauth-authorization-server') {
|
|
106
|
+
return new Response('Not Found', { status: 404 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── MCP Streamable HTTP transport on root (/) and /mcp ─────────────
|
|
110
|
+
|
|
111
|
+
if (pathname === '/' || pathname === '' || pathname === '/mcp') {
|
|
112
|
+
if (request.method === 'GET') {
|
|
113
|
+
const accept = request.headers.get('Accept') || '';
|
|
114
|
+
if (accept.includes('text/event-stream')) {
|
|
115
|
+
return new Response('Method Not Allowed', { status: 405, headers: CORS_HEADERS });
|
|
116
|
+
}
|
|
117
|
+
return json({
|
|
118
|
+
name: 'Symbols MCP',
|
|
119
|
+
version: '1.0.11',
|
|
120
|
+
description: 'MCP server for Symbols.app — documentation search and framework reference',
|
|
121
|
+
endpoints: {
|
|
122
|
+
mcp: 'POST / (Streamable HTTP transport)',
|
|
123
|
+
health: 'GET /health',
|
|
124
|
+
tools: 'GET /tools',
|
|
125
|
+
resources: 'GET /resources',
|
|
126
|
+
prompts: 'GET /prompts',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
47
129
|
}
|
|
48
130
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const results = body.map(handleJsonRpc).filter(Boolean);
|
|
52
|
-
return json(results);
|
|
131
|
+
if (request.method === 'DELETE') {
|
|
132
|
+
return new Response(null, { status: 405, headers: CORS_HEADERS });
|
|
53
133
|
}
|
|
54
134
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
135
|
+
if (request.method === 'POST') {
|
|
136
|
+
let body;
|
|
137
|
+
try {
|
|
138
|
+
body = await request.json();
|
|
139
|
+
} catch {
|
|
140
|
+
return json({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' } }, 400);
|
|
141
|
+
}
|
|
142
|
+
return handleMcpPost(request, body);
|
|
143
|
+
}
|
|
58
144
|
}
|
|
59
145
|
|
|
60
|
-
// ──
|
|
146
|
+
// ── Health check ────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
if (pathname === '/health' && request.method === 'GET') {
|
|
149
|
+
return json({ alive: true, service: 'symbols-mcp', runtime: 'cloudflare-worker' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── REST endpoints ─────────────────────────────────────────────────
|
|
61
153
|
|
|
62
|
-
// GET /tools — list all tools
|
|
63
154
|
if (pathname === '/tools' && request.method === 'GET') {
|
|
64
155
|
return json({ tools: TOOLS });
|
|
65
156
|
}
|
|
66
157
|
|
|
67
|
-
// POST /tools/:name — call a tool
|
|
68
158
|
if (pathname.startsWith('/tools/') && request.method === 'POST') {
|
|
69
159
|
const toolName = pathname.slice('/tools/'.length);
|
|
70
160
|
let args = {};
|
|
@@ -74,7 +164,6 @@ export async function handleRequest(request) {
|
|
|
74
164
|
} catch {
|
|
75
165
|
return json({ error: 'Invalid JSON body' }, 400);
|
|
76
166
|
}
|
|
77
|
-
|
|
78
167
|
try {
|
|
79
168
|
const result = callTool(toolName, args);
|
|
80
169
|
return json({ content: [{ type: 'text', text: result }] });
|
|
@@ -83,12 +172,10 @@ export async function handleRequest(request) {
|
|
|
83
172
|
}
|
|
84
173
|
}
|
|
85
174
|
|
|
86
|
-
// GET /resources — list all resources
|
|
87
175
|
if (pathname === '/resources' && request.method === 'GET') {
|
|
88
176
|
return json({ resources: listResources() });
|
|
89
177
|
}
|
|
90
178
|
|
|
91
|
-
// GET /resources/:uri — read a resource (uri passed as query param)
|
|
92
179
|
if (pathname === '/resources/read' && request.method === 'GET') {
|
|
93
180
|
const uri = url.searchParams.get('uri');
|
|
94
181
|
if (!uri) return json({ error: 'Missing ?uri= parameter' }, 400);
|
|
@@ -97,10 +184,9 @@ export async function handleRequest(request) {
|
|
|
97
184
|
return json({ contents: [{ uri, mimeType: 'text/markdown', text: content }] });
|
|
98
185
|
}
|
|
99
186
|
|
|
100
|
-
// GET /prompts — list all prompts
|
|
101
187
|
if (pathname === '/prompts' && request.method === 'GET') {
|
|
102
188
|
return json({ prompts: PROMPTS });
|
|
103
189
|
}
|
|
104
190
|
|
|
105
|
-
return
|
|
191
|
+
return new Response('Not Found', { status: 404 });
|
|
106
192
|
}
|
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
|