signal-relay-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +239 -0
- package/package.json +59 -0
- package/src/api-client.ts +311 -0
- package/src/index.ts +661 -0
- package/src/tools.ts +233 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocioLogic MCP Server - Cloudflare Workers Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This is a remote MCP server that provides access to the SocioLogic
|
|
5
|
+
* Revenue Intelligence Platform via the Model Context Protocol.
|
|
6
|
+
*
|
|
7
|
+
* Deploy to Cloudflare Workers for global edge deployment.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
12
|
+
import { SocioLogicClient } from "./api-client";
|
|
13
|
+
import {
|
|
14
|
+
TOOL_DEFINITIONS,
|
|
15
|
+
ListPersonasSchema,
|
|
16
|
+
GetPersonaSchema,
|
|
17
|
+
CreatePersonaSchema,
|
|
18
|
+
InterviewPersonaSchema,
|
|
19
|
+
GetPersonaMemoriesSchema,
|
|
20
|
+
ListCampaignsSchema,
|
|
21
|
+
GetCampaignSchema,
|
|
22
|
+
CreateCampaignSchema,
|
|
23
|
+
ExecuteCampaignSchema,
|
|
24
|
+
ExportCampaignSchema,
|
|
25
|
+
ListFocusGroupsSchema,
|
|
26
|
+
GetFocusGroupSchema,
|
|
27
|
+
CreateFocusGroupSchema,
|
|
28
|
+
AddPersonasToFocusGroupSchema,
|
|
29
|
+
} from "./tools";
|
|
30
|
+
|
|
31
|
+
// ============================================
|
|
32
|
+
// VALIDATION HELPER
|
|
33
|
+
// ============================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Safely parse input with Zod and return clean error messages.
|
|
37
|
+
* Avoids exposing internal schema details in error responses.
|
|
38
|
+
*/
|
|
39
|
+
function safeParseArgs<T extends z.ZodSchema>(
|
|
40
|
+
schema: T,
|
|
41
|
+
args: unknown
|
|
42
|
+
): z.infer<T> {
|
|
43
|
+
const result = schema.safeParse(args);
|
|
44
|
+
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
// Create a clean error message without exposing schema internals
|
|
47
|
+
const errors = result.error.errors.map((err) => {
|
|
48
|
+
const path = err.path.length > 0 ? `${err.path.join(".")}: ` : "";
|
|
49
|
+
return `${path}${err.message}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
throw new Error(`Invalid parameters: ${errors.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// TYPES
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
interface Env {
|
|
63
|
+
SOCIOLOGIC_API_URL?: string;
|
|
64
|
+
// KV namespace for session storage (optional, for OAuth)
|
|
65
|
+
SESSION_STORE?: KVNamespace;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface MCPRequest {
|
|
69
|
+
jsonrpc: "2.0";
|
|
70
|
+
id: string | number;
|
|
71
|
+
method: string;
|
|
72
|
+
params?: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface MCPResponse {
|
|
76
|
+
jsonrpc: "2.0";
|
|
77
|
+
id: string | number;
|
|
78
|
+
result?: unknown;
|
|
79
|
+
error?: {
|
|
80
|
+
code: number;
|
|
81
|
+
message: string;
|
|
82
|
+
data?: unknown;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MCP Error Codes
|
|
87
|
+
const MCP_ERRORS = {
|
|
88
|
+
PARSE_ERROR: -32700,
|
|
89
|
+
INVALID_REQUEST: -32600,
|
|
90
|
+
METHOD_NOT_FOUND: -32601,
|
|
91
|
+
INVALID_PARAMS: -32602,
|
|
92
|
+
INTERNAL_ERROR: -32603,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Maximum request body size (1MB - generous for JSON-RPC)
|
|
96
|
+
const MAX_REQUEST_SIZE = 1024 * 1024;
|
|
97
|
+
|
|
98
|
+
// ============================================
|
|
99
|
+
// MCP PROTOCOL HANDLER
|
|
100
|
+
// ============================================
|
|
101
|
+
|
|
102
|
+
class MCPHandler {
|
|
103
|
+
private client: SocioLogicClient;
|
|
104
|
+
|
|
105
|
+
constructor(apiUrl: string, apiKey: string) {
|
|
106
|
+
this.client = new SocioLogicClient({ apiUrl, apiKey });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async handleRequest(request: MCPRequest): Promise<MCPResponse> {
|
|
110
|
+
const { id, method, params } = request;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
switch (method) {
|
|
114
|
+
case "initialize":
|
|
115
|
+
return await this.handleInitialize(id);
|
|
116
|
+
|
|
117
|
+
case "tools/list":
|
|
118
|
+
return this.handleToolsList(id);
|
|
119
|
+
|
|
120
|
+
case "tools/call":
|
|
121
|
+
return this.handleToolsCall(id, params as { name: string; arguments: unknown });
|
|
122
|
+
|
|
123
|
+
case "ping":
|
|
124
|
+
return { jsonrpc: "2.0", id, result: { pong: true } };
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
return {
|
|
128
|
+
jsonrpc: "2.0",
|
|
129
|
+
id,
|
|
130
|
+
error: {
|
|
131
|
+
code: MCP_ERRORS.METHOD_NOT_FOUND,
|
|
132
|
+
message: `Method not found: ${method}`,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("MCP handler error:", error);
|
|
138
|
+
return {
|
|
139
|
+
jsonrpc: "2.0",
|
|
140
|
+
id,
|
|
141
|
+
error: {
|
|
142
|
+
code: MCP_ERRORS.INTERNAL_ERROR,
|
|
143
|
+
message: error instanceof Error ? error.message : "Internal error",
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async handleInitialize(id: string | number): Promise<MCPResponse> {
|
|
150
|
+
// Validate API key by making a lightweight call to check credits
|
|
151
|
+
const validation = await this.client.getCreditsBalance();
|
|
152
|
+
|
|
153
|
+
if (validation.error) {
|
|
154
|
+
return {
|
|
155
|
+
jsonrpc: "2.0",
|
|
156
|
+
id,
|
|
157
|
+
error: {
|
|
158
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
159
|
+
message: `API key validation failed: ${validation.error.message}`,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
jsonrpc: "2.0",
|
|
166
|
+
id,
|
|
167
|
+
result: {
|
|
168
|
+
protocolVersion: "2024-11-05",
|
|
169
|
+
capabilities: {
|
|
170
|
+
tools: {},
|
|
171
|
+
},
|
|
172
|
+
serverInfo: {
|
|
173
|
+
name: "sociologic-mcp-server",
|
|
174
|
+
version: "1.0.0",
|
|
175
|
+
description: "SocioLogic Revenue Intelligence Platform - High-fidelity synthetic personas for market research",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private handleToolsList(id: string | number): MCPResponse {
|
|
182
|
+
const tools = TOOL_DEFINITIONS.map((tool) => {
|
|
183
|
+
// Convert Zod schema to JSON Schema using the proper library
|
|
184
|
+
const jsonSchema = zodToJsonSchema(tool.inputSchema, {
|
|
185
|
+
target: "jsonSchema7",
|
|
186
|
+
$refStrategy: "none",
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
name: tool.name,
|
|
191
|
+
description: tool.description,
|
|
192
|
+
inputSchema: jsonSchema,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
jsonrpc: "2.0",
|
|
198
|
+
id,
|
|
199
|
+
result: { tools },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async handleToolsCall(
|
|
204
|
+
id: string | number,
|
|
205
|
+
params: { name: string; arguments: unknown }
|
|
206
|
+
): Promise<MCPResponse> {
|
|
207
|
+
const { name, arguments: args } = params;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const result = await this.executeTool(name, args);
|
|
211
|
+
return {
|
|
212
|
+
jsonrpc: "2.0",
|
|
213
|
+
id,
|
|
214
|
+
result: {
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: "text",
|
|
218
|
+
text: JSON.stringify(result, null, 2),
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
jsonrpc: "2.0",
|
|
226
|
+
id,
|
|
227
|
+
error: {
|
|
228
|
+
code: MCP_ERRORS.INTERNAL_ERROR,
|
|
229
|
+
message: error instanceof Error ? error.message : "Tool execution failed",
|
|
230
|
+
data: { tool: name },
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async executeTool(name: string, args: unknown): Promise<unknown> {
|
|
237
|
+
switch (name) {
|
|
238
|
+
case "sociologic_list_personas": {
|
|
239
|
+
const parsed = safeParseArgs(ListPersonasSchema, args);
|
|
240
|
+
return this.client.listPersonas(parsed);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case "sociologic_get_persona": {
|
|
244
|
+
const parsed = safeParseArgs(GetPersonaSchema, args);
|
|
245
|
+
return this.client.getPersona(parsed.slug);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case "sociologic_create_persona": {
|
|
249
|
+
const parsed = safeParseArgs(CreatePersonaSchema, args);
|
|
250
|
+
return this.client.createPersona(parsed);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case "sociologic_interview_persona": {
|
|
254
|
+
const parsed = safeParseArgs(InterviewPersonaSchema, args);
|
|
255
|
+
return this.client.interviewPersona(parsed.slug, {
|
|
256
|
+
message: parsed.message,
|
|
257
|
+
conversation_id: parsed.conversation_id,
|
|
258
|
+
include_memory: parsed.include_memory,
|
|
259
|
+
save_conversation: parsed.save_conversation,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case "sociologic_get_persona_memories": {
|
|
264
|
+
const parsed = safeParseArgs(GetPersonaMemoriesSchema, args);
|
|
265
|
+
return this.client.getPersonaMemories(parsed.slug, {
|
|
266
|
+
query: parsed.query,
|
|
267
|
+
limit: parsed.limit,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case "sociologic_list_campaigns": {
|
|
272
|
+
const parsed = safeParseArgs(ListCampaignsSchema, args);
|
|
273
|
+
return this.client.listCampaigns(parsed);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case "sociologic_get_campaign": {
|
|
277
|
+
const parsed = safeParseArgs(GetCampaignSchema, args);
|
|
278
|
+
return this.client.getCampaign(parsed.id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case "sociologic_create_campaign": {
|
|
282
|
+
const parsed = safeParseArgs(CreateCampaignSchema, args);
|
|
283
|
+
return this.client.createCampaign(parsed);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
case "sociologic_execute_campaign": {
|
|
287
|
+
const parsed = safeParseArgs(ExecuteCampaignSchema, args);
|
|
288
|
+
return this.client.executeCampaign(parsed.id);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case "sociologic_export_campaign": {
|
|
292
|
+
const parsed = safeParseArgs(ExportCampaignSchema, args);
|
|
293
|
+
return this.client.exportCampaign(parsed.id, parsed.format);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case "sociologic_list_focus_groups": {
|
|
297
|
+
const parsed = safeParseArgs(ListFocusGroupsSchema, args);
|
|
298
|
+
return this.client.listFocusGroups(parsed);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case "sociologic_get_focus_group": {
|
|
302
|
+
const parsed = safeParseArgs(GetFocusGroupSchema, args);
|
|
303
|
+
return this.client.getFocusGroup(parsed.id);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case "sociologic_create_focus_group": {
|
|
307
|
+
const parsed = safeParseArgs(CreateFocusGroupSchema, args);
|
|
308
|
+
return this.client.createFocusGroup(parsed);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case "sociologic_add_personas_to_focus_group": {
|
|
312
|
+
const parsed = safeParseArgs(AddPersonasToFocusGroupSchema, args);
|
|
313
|
+
return this.client.addPersonasToFocusGroup(
|
|
314
|
+
parsed.focus_group_id,
|
|
315
|
+
parsed.persona_ids
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case "sociologic_get_credits_balance": {
|
|
320
|
+
return this.client.getCreditsBalance();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
default:
|
|
324
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ============================================
|
|
330
|
+
// SSE TRANSPORT - NOT IMPLEMENTED
|
|
331
|
+
// ============================================
|
|
332
|
+
// SSE transport requires bidirectional communication and stateful connections
|
|
333
|
+
// which would need Cloudflare Durable Objects. Use JSON-RPC endpoint instead.
|
|
334
|
+
// See: https://modelcontextprotocol.io/docs/concepts/transports
|
|
335
|
+
|
|
336
|
+
// ============================================
|
|
337
|
+
// CLOUDFLARE WORKERS HANDLER
|
|
338
|
+
// ============================================
|
|
339
|
+
|
|
340
|
+
export default {
|
|
341
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
342
|
+
const url = new URL(request.url);
|
|
343
|
+
|
|
344
|
+
// Handle CORS preflight
|
|
345
|
+
if (request.method === "OPTIONS") {
|
|
346
|
+
return new Response(null, {
|
|
347
|
+
status: 204,
|
|
348
|
+
headers: {
|
|
349
|
+
"Access-Control-Allow-Origin": "*",
|
|
350
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
351
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
|
|
352
|
+
"Access-Control-Max-Age": "86400",
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Early check of Content-Length header if present (optimization)
|
|
358
|
+
const contentLength = request.headers.get("Content-Length");
|
|
359
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_REQUEST_SIZE) {
|
|
360
|
+
return new Response(
|
|
361
|
+
JSON.stringify({
|
|
362
|
+
jsonrpc: "2.0",
|
|
363
|
+
id: null,
|
|
364
|
+
error: {
|
|
365
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
366
|
+
message: `Request body too large. Maximum size is ${MAX_REQUEST_SIZE} bytes.`,
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
{
|
|
370
|
+
status: 413,
|
|
371
|
+
headers: {
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
"Access-Control-Allow-Origin": "*",
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Extract API key from request (headers only - never from query params for security)
|
|
380
|
+
// Trim whitespace and handle empty strings
|
|
381
|
+
const apiKey = (
|
|
382
|
+
request.headers.get("X-API-Key") ||
|
|
383
|
+
request.headers.get("Authorization")?.replace("Bearer ", "")
|
|
384
|
+
)?.trim();
|
|
385
|
+
|
|
386
|
+
if (!apiKey) {
|
|
387
|
+
return new Response(
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
error: {
|
|
390
|
+
code: "AUTHENTICATION_REQUIRED",
|
|
391
|
+
message: "API key required. Pass via X-API-Key header or Authorization: Bearer header.",
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
394
|
+
{
|
|
395
|
+
status: 401,
|
|
396
|
+
headers: {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
"Access-Control-Allow-Origin": "*",
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const apiUrl = env.SOCIOLOGIC_API_URL || "https://www.sociologic.ai";
|
|
405
|
+
const handler = new MCPHandler(apiUrl, apiKey);
|
|
406
|
+
|
|
407
|
+
// Route based on path
|
|
408
|
+
const path = url.pathname;
|
|
409
|
+
|
|
410
|
+
// SSE endpoint - not implemented (requires Durable Objects for stateful connections)
|
|
411
|
+
if (path === "/sse" || path === "/mcp/sse") {
|
|
412
|
+
return new Response(
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
error: {
|
|
415
|
+
code: "NOT_IMPLEMENTED",
|
|
416
|
+
message: "SSE transport is not available. Use JSON-RPC instead: POST to / or /rpc",
|
|
417
|
+
documentation: "https://www.sociologic.ai/docs/mcp",
|
|
418
|
+
},
|
|
419
|
+
}),
|
|
420
|
+
{
|
|
421
|
+
status: 501,
|
|
422
|
+
headers: {
|
|
423
|
+
"Content-Type": "application/json",
|
|
424
|
+
"Access-Control-Allow-Origin": "*",
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// JSON-RPC endpoint for direct calls
|
|
431
|
+
if (path === "/" || path === "/mcp" || path === "/rpc") {
|
|
432
|
+
if (request.method !== "POST") {
|
|
433
|
+
return new Response(
|
|
434
|
+
JSON.stringify({
|
|
435
|
+
jsonrpc: "2.0",
|
|
436
|
+
error: {
|
|
437
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
438
|
+
message: "POST method required for JSON-RPC",
|
|
439
|
+
},
|
|
440
|
+
}),
|
|
441
|
+
{
|
|
442
|
+
status: 405,
|
|
443
|
+
headers: {
|
|
444
|
+
"Content-Type": "application/json",
|
|
445
|
+
"Access-Control-Allow-Origin": "*",
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Read body as text first to validate size (bypasses Content-Length bypass)
|
|
453
|
+
const bodyText = await request.text();
|
|
454
|
+
if (bodyText.length > MAX_REQUEST_SIZE) {
|
|
455
|
+
return new Response(
|
|
456
|
+
JSON.stringify({
|
|
457
|
+
jsonrpc: "2.0",
|
|
458
|
+
id: null,
|
|
459
|
+
error: {
|
|
460
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
461
|
+
message: `Request body too large. Maximum size is ${MAX_REQUEST_SIZE} bytes.`,
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
{
|
|
465
|
+
status: 413,
|
|
466
|
+
headers: {
|
|
467
|
+
"Content-Type": "application/json",
|
|
468
|
+
"Access-Control-Allow-Origin": "*",
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Parse JSON
|
|
475
|
+
let body: unknown;
|
|
476
|
+
try {
|
|
477
|
+
body = JSON.parse(bodyText);
|
|
478
|
+
} catch {
|
|
479
|
+
return new Response(
|
|
480
|
+
JSON.stringify({
|
|
481
|
+
jsonrpc: "2.0",
|
|
482
|
+
id: null,
|
|
483
|
+
error: {
|
|
484
|
+
code: MCP_ERRORS.PARSE_ERROR,
|
|
485
|
+
message: "Failed to parse JSON body",
|
|
486
|
+
},
|
|
487
|
+
}),
|
|
488
|
+
{
|
|
489
|
+
status: 400,
|
|
490
|
+
headers: {
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
"Access-Control-Allow-Origin": "*",
|
|
493
|
+
},
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Validate body is an object (not array, null, or primitive)
|
|
499
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
500
|
+
return new Response(
|
|
501
|
+
JSON.stringify({
|
|
502
|
+
jsonrpc: "2.0",
|
|
503
|
+
id: null,
|
|
504
|
+
error: {
|
|
505
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
506
|
+
message: "Request body must be a JSON object",
|
|
507
|
+
},
|
|
508
|
+
}),
|
|
509
|
+
{
|
|
510
|
+
status: 400,
|
|
511
|
+
headers: {
|
|
512
|
+
"Content-Type": "application/json",
|
|
513
|
+
"Access-Control-Allow-Origin": "*",
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const mcpRequest = body as MCPRequest;
|
|
520
|
+
|
|
521
|
+
// Validate JSON-RPC structure
|
|
522
|
+
if (mcpRequest.jsonrpc !== "2.0" || typeof mcpRequest.method !== "string") {
|
|
523
|
+
return new Response(
|
|
524
|
+
JSON.stringify({
|
|
525
|
+
jsonrpc: "2.0",
|
|
526
|
+
id: mcpRequest.id ?? null,
|
|
527
|
+
error: {
|
|
528
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
529
|
+
message: "Invalid JSON-RPC 2.0 request: requires jsonrpc='2.0' and method string",
|
|
530
|
+
},
|
|
531
|
+
}),
|
|
532
|
+
{
|
|
533
|
+
status: 400,
|
|
534
|
+
headers: {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
"Access-Control-Allow-Origin": "*",
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Validate id is string, number, or null (per JSON-RPC spec)
|
|
543
|
+
if (
|
|
544
|
+
mcpRequest.id !== undefined &&
|
|
545
|
+
mcpRequest.id !== null &&
|
|
546
|
+
typeof mcpRequest.id !== "string" &&
|
|
547
|
+
typeof mcpRequest.id !== "number"
|
|
548
|
+
) {
|
|
549
|
+
return new Response(
|
|
550
|
+
JSON.stringify({
|
|
551
|
+
jsonrpc: "2.0",
|
|
552
|
+
id: null,
|
|
553
|
+
error: {
|
|
554
|
+
code: MCP_ERRORS.INVALID_REQUEST,
|
|
555
|
+
message: "Invalid JSON-RPC request: id must be string, number, or null",
|
|
556
|
+
},
|
|
557
|
+
}),
|
|
558
|
+
{
|
|
559
|
+
status: 400,
|
|
560
|
+
headers: {
|
|
561
|
+
"Content-Type": "application/json",
|
|
562
|
+
"Access-Control-Allow-Origin": "*",
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const response = await handler.handleRequest(mcpRequest);
|
|
569
|
+
|
|
570
|
+
return new Response(JSON.stringify(response), {
|
|
571
|
+
headers: {
|
|
572
|
+
"Content-Type": "application/json",
|
|
573
|
+
"Access-Control-Allow-Origin": "*",
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
} catch (error) {
|
|
577
|
+
return new Response(
|
|
578
|
+
JSON.stringify({
|
|
579
|
+
jsonrpc: "2.0",
|
|
580
|
+
id: null,
|
|
581
|
+
error: {
|
|
582
|
+
code: MCP_ERRORS.INTERNAL_ERROR,
|
|
583
|
+
message: "Failed to process request",
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
{
|
|
587
|
+
status: 500,
|
|
588
|
+
headers: {
|
|
589
|
+
"Content-Type": "application/json",
|
|
590
|
+
"Access-Control-Allow-Origin": "*",
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Health check endpoint
|
|
598
|
+
if (path === "/health") {
|
|
599
|
+
return new Response(
|
|
600
|
+
JSON.stringify({
|
|
601
|
+
status: "healthy",
|
|
602
|
+
server: "sociologic-mcp-server",
|
|
603
|
+
version: "1.0.0",
|
|
604
|
+
timestamp: new Date().toISOString(),
|
|
605
|
+
}),
|
|
606
|
+
{
|
|
607
|
+
headers: {
|
|
608
|
+
"Content-Type": "application/json",
|
|
609
|
+
"Access-Control-Allow-Origin": "*",
|
|
610
|
+
},
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Info endpoint
|
|
616
|
+
if (path === "/info") {
|
|
617
|
+
return new Response(
|
|
618
|
+
JSON.stringify({
|
|
619
|
+
name: "SocioLogic MCP Server",
|
|
620
|
+
version: "1.0.0",
|
|
621
|
+
description: "Remote MCP server for the SocioLogic Revenue Intelligence Platform",
|
|
622
|
+
endpoints: {
|
|
623
|
+
"/": "JSON-RPC endpoint (POST)",
|
|
624
|
+
"/sse": "Not implemented (returns 501)",
|
|
625
|
+
"/health": "Health check",
|
|
626
|
+
"/info": "Server information",
|
|
627
|
+
},
|
|
628
|
+
tools: TOOL_DEFINITIONS.map((t) => ({
|
|
629
|
+
name: t.name,
|
|
630
|
+
description: t.description,
|
|
631
|
+
})),
|
|
632
|
+
documentation: "https://www.sociologic.ai/docs",
|
|
633
|
+
}),
|
|
634
|
+
{
|
|
635
|
+
headers: {
|
|
636
|
+
"Content-Type": "application/json",
|
|
637
|
+
"Access-Control-Allow-Origin": "*",
|
|
638
|
+
},
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 404 for unknown paths
|
|
644
|
+
return new Response(
|
|
645
|
+
JSON.stringify({
|
|
646
|
+
error: {
|
|
647
|
+
code: "NOT_FOUND",
|
|
648
|
+
message: `Unknown endpoint: ${path}`,
|
|
649
|
+
available_endpoints: ["/", "/sse", "/health", "/info"],
|
|
650
|
+
},
|
|
651
|
+
}),
|
|
652
|
+
{
|
|
653
|
+
status: 404,
|
|
654
|
+
headers: {
|
|
655
|
+
"Content-Type": "application/json",
|
|
656
|
+
"Access-Control-Allow-Origin": "*",
|
|
657
|
+
},
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
},
|
|
661
|
+
};
|