@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/mcp-server",
3
- "version": "3.6.6",
3
+ "version": "3.6.7",
4
4
  "description": "HTTP proxy for the Symbols MCP server — runs as a Cloudflare Worker",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Cloudflare Worker request handler for Symbols MCP HTTP proxy.
2
+ * Cloudflare Worker request handler for Symbols MCP — Streamable HTTP transport.
3
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
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
- // ── 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);
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
- // Support batch requests
50
- if (Array.isArray(body)) {
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
- const result = handleJsonRpc(body);
56
- if (result === null) return new Response(null, { status: 204 });
57
- return json(result);
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
- // ── REST endpoints ─────────────────────────────────────────────────────
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 json({ error: 'Not found' }, 404);
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 = ["nodejs_compat_v2"]
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 (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"
41
+ # ── Routes ────────────────────────────────────────────────────────────────────
42
+
43
+ # Route handled via Cloudflare DNS CNAME → workers.dev