@socketsecurity/mcp 0.0.7 → 0.0.9
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/README.md +285 -106
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -0
- package/index.js +451 -0
- package/mock-client/debug-client.d.ts +3 -0
- package/mock-client/debug-client.d.ts.map +1 -0
- package/{build/mock-client → mock-client}/debug-client.js +6 -10
- package/mock-client/http-client.d.ts +3 -0
- package/mock-client/http-client.d.ts.map +1 -0
- package/{build/mock-client → mock-client}/http-client.js +11 -23
- package/mock-client/stdio-client.d.ts +3 -0
- package/mock-client/stdio-client.d.ts.map +1 -0
- package/{build/mock-client → mock-client}/stdio-client.js +7 -8
- package/package.json +36 -15
- package/build/index.js +0 -348
- package/build/index.js.map +0 -1
- package/build/mock-client/debug-client.js.map +0 -1
- package/build/mock-client/http-client.js.map +0 -1
- package/build/mock-client/stdio-client.js.map +0 -1
package/index.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
#!/usr/bin/env node --experimental-strip-types
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { pino } from 'pino';
|
|
7
|
+
import readline from 'readline';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
import { createServer } from 'http';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
const __dirname = import.meta.dirname;
|
|
15
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, './package.json'), 'utf8'));
|
|
16
|
+
const VERSION = packageJson.version || '0.0.1';
|
|
17
|
+
const logger = pino({
|
|
18
|
+
level: 'info',
|
|
19
|
+
transport: {
|
|
20
|
+
targets: [
|
|
21
|
+
{
|
|
22
|
+
target: 'pino/file',
|
|
23
|
+
options: { destination: join(tmpdir(), 'socket-mcp-error.log') },
|
|
24
|
+
level: 'error'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
target: 'pino/file',
|
|
28
|
+
options: { destination: join(tmpdir(), 'socket-mcp.log') },
|
|
29
|
+
level: 'info'
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const SOCKET_API_URL = process.env['SOCKET_DEBUG'] === 'true'
|
|
35
|
+
? 'http://localhost:8866/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false'
|
|
36
|
+
: 'https://api.socket.dev/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false';
|
|
37
|
+
async function getApiKeyInteractively() {
|
|
38
|
+
const rl = readline.createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stderr
|
|
41
|
+
});
|
|
42
|
+
const apiKey = await new Promise((resolve) => {
|
|
43
|
+
rl.question('Please enter your Socket API key: ', (answer) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
resolve(answer);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
logger.error('No API key provided');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
return apiKey;
|
|
53
|
+
}
|
|
54
|
+
let SOCKET_API_KEY = process.env['SOCKET_API_KEY'] || '';
|
|
55
|
+
const transports = {};
|
|
56
|
+
const server = new McpServer({
|
|
57
|
+
name: 'socket',
|
|
58
|
+
version: VERSION,
|
|
59
|
+
description: 'Socket MCP server',
|
|
60
|
+
capabilities: {
|
|
61
|
+
resources: {},
|
|
62
|
+
tools: {},
|
|
63
|
+
streaming: {}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
server.tool('depscore', "Get the dependency score of packages with the `depscore` tool from Socket. Use 'unknown' for version if not known. Use this tool to scan dependencies for their quality and security on existing code or when code is generated. Stop generating code and ask the user how to proceed when any of the scores are low. When checking dependencies, make sure to also check the imports in the code, not just the manifest files (pyproject.toml, package.json, etc).", {
|
|
67
|
+
packages: z.array(z.object({
|
|
68
|
+
ecosystem: z.string().describe('The package ecosystem (e.g., npm, pypi)').default('npm'),
|
|
69
|
+
depname: z.string().describe('The name of the dependency'),
|
|
70
|
+
version: z.string().describe("The version of the dependency, use 'unknown' if not known").default('unknown'),
|
|
71
|
+
})).describe('Array of packages to check'),
|
|
72
|
+
}, async ({ packages }) => {
|
|
73
|
+
logger.info(`Received request for ${packages.length} packages`);
|
|
74
|
+
const SOCKET_HEADERS = {
|
|
75
|
+
'user-agent': `socket-mcp/${VERSION}`,
|
|
76
|
+
accept: 'application/x-ndjson',
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
authorization: `Bearer ${SOCKET_API_KEY}`
|
|
79
|
+
};
|
|
80
|
+
const components = packages.map(pkg => {
|
|
81
|
+
const cleanedVersion = pkg.version.replace(/[\^~]/g, '');
|
|
82
|
+
let purl;
|
|
83
|
+
if (cleanedVersion === '1.0.0' || cleanedVersion === 'unknown' || !cleanedVersion) {
|
|
84
|
+
purl = `pkg:${pkg.ecosystem}/${pkg.depname}`;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`);
|
|
88
|
+
purl = `pkg:${pkg.ecosystem}/${pkg.depname}@${cleanedVersion}`;
|
|
89
|
+
}
|
|
90
|
+
return { purl };
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(SOCKET_API_URL, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: SOCKET_HEADERS,
|
|
96
|
+
body: JSON.stringify({ components })
|
|
97
|
+
});
|
|
98
|
+
const responseText = await response.text();
|
|
99
|
+
if (response.status !== 200) {
|
|
100
|
+
const errorMsg = `Error processing packages: [${response.status}] ${responseText}`;
|
|
101
|
+
logger.error(errorMsg);
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: errorMsg }],
|
|
104
|
+
isError: true
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
else if (!responseText.trim()) {
|
|
108
|
+
const errorMsg = 'No packages were found.';
|
|
109
|
+
logger.error(errorMsg);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: errorMsg }],
|
|
112
|
+
isError: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const results = [];
|
|
117
|
+
if ((response.headers.get('content-type') || '').includes('x-ndjson')) {
|
|
118
|
+
const jsonLines = responseText.split('\n')
|
|
119
|
+
.filter(line => line.trim())
|
|
120
|
+
.map(line => JSON.parse(line));
|
|
121
|
+
if (!jsonLines.length) {
|
|
122
|
+
const errorMsg = 'No valid JSON objects found in NDJSON response';
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: errorMsg }],
|
|
125
|
+
isError: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
for (const jsonData of jsonLines) {
|
|
129
|
+
const purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
|
|
130
|
+
if (jsonData.score && jsonData.score.overall !== undefined) {
|
|
131
|
+
const scoreEntries = Object.entries(jsonData.score)
|
|
132
|
+
.filter(([key]) => key !== 'overall' && key !== 'uuid')
|
|
133
|
+
.map(([key, value]) => {
|
|
134
|
+
const numValue = Number(value);
|
|
135
|
+
const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue;
|
|
136
|
+
return `${key}: ${displayValue}`;
|
|
137
|
+
})
|
|
138
|
+
.join(', ');
|
|
139
|
+
results.push(`${purl}: ${scoreEntries}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
results.push(`${purl}: No score found`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const jsonData = JSON.parse(responseText);
|
|
148
|
+
const purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
|
|
149
|
+
if (jsonData.score && jsonData.score.overall !== undefined) {
|
|
150
|
+
const scoreEntries = Object.entries(jsonData.score)
|
|
151
|
+
.filter(([key]) => key !== 'overall' && key !== 'uuid')
|
|
152
|
+
.map(([key, value]) => {
|
|
153
|
+
const numValue = Number(value);
|
|
154
|
+
const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue;
|
|
155
|
+
return `${key}: ${displayValue}`;
|
|
156
|
+
})
|
|
157
|
+
.join(', ');
|
|
158
|
+
results.push(`${purl}: ${scoreEntries}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: results.length > 0
|
|
166
|
+
? `Dependency scores:\n${results.join('\n')}`
|
|
167
|
+
: 'No scores found for the provided packages'
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
const error = e;
|
|
174
|
+
const errorMsg = `JSON parsing error: ${error.message} -- Response: ${responseText}`;
|
|
175
|
+
logger.error(errorMsg);
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: 'text', text: 'Error parsing response from Socket API' }],
|
|
178
|
+
isError: true
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
const error = e;
|
|
184
|
+
const errorMsg = `Error processing packages: ${error.message}`;
|
|
185
|
+
logger.error(errorMsg);
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: 'text', text: 'Error connecting to Socket API' }],
|
|
188
|
+
isError: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
const useHttp = process.env['MCP_HTTP_MODE'] === 'true' || process.argv.includes('--http');
|
|
193
|
+
const port = parseInt(process.env['MCP_PORT'] || '3000', 10);
|
|
194
|
+
if (!SOCKET_API_KEY) {
|
|
195
|
+
if (useHttp) {
|
|
196
|
+
logger.error('SOCKET_API_KEY environment variable is not set');
|
|
197
|
+
SOCKET_API_KEY = await getApiKeyInteractively();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
logger.error('SOCKET_API_KEY environment variable is required in stdio mode');
|
|
201
|
+
logger.error('Please set the SOCKET_API_KEY environment variable and try again');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (useHttp) {
|
|
206
|
+
logger.info(`Starting HTTP server on port ${port}`);
|
|
207
|
+
const httpServer = createServer(async (req, res) => {
|
|
208
|
+
const origin = req.headers.origin;
|
|
209
|
+
const allowedOrigins = [
|
|
210
|
+
'http://localhost:3000',
|
|
211
|
+
'http://127.0.0.1:3000',
|
|
212
|
+
'https://mcp.socket.dev',
|
|
213
|
+
'https://mcp.socket-staging.dev'
|
|
214
|
+
];
|
|
215
|
+
const isValidOrigin = !origin || allowedOrigins.includes(origin);
|
|
216
|
+
if (origin && !isValidOrigin) {
|
|
217
|
+
logger.warn(`Rejected request from invalid origin: ${origin}`);
|
|
218
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
219
|
+
res.end(JSON.stringify({
|
|
220
|
+
jsonrpc: '2.0',
|
|
221
|
+
error: { code: -32000, message: 'Forbidden: Invalid origin' },
|
|
222
|
+
id: null
|
|
223
|
+
}));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (origin && isValidOrigin) {
|
|
227
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
231
|
+
}
|
|
232
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
233
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Accept, Last-Event-ID');
|
|
234
|
+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
|
|
235
|
+
if (req.method === 'OPTIONS') {
|
|
236
|
+
res.writeHead(200);
|
|
237
|
+
res.end();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
241
|
+
if (url.pathname === '/health') {
|
|
242
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify({
|
|
244
|
+
status: 'healthy',
|
|
245
|
+
service: 'socket-mcp',
|
|
246
|
+
version: VERSION,
|
|
247
|
+
timestamp: new Date().toISOString()
|
|
248
|
+
}));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (url.pathname === '/') {
|
|
252
|
+
if (req.method === 'POST') {
|
|
253
|
+
const acceptHeader = req.headers.accept;
|
|
254
|
+
if (!acceptHeader || (!acceptHeader.includes('application/json') && !acceptHeader.includes('text/event-stream'))) {
|
|
255
|
+
logger.warn(`Invalid Accept header: ${acceptHeader}`);
|
|
256
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
257
|
+
res.end(JSON.stringify({
|
|
258
|
+
jsonrpc: '2.0',
|
|
259
|
+
error: { code: -32000, message: 'Bad Request: Accept header must include application/json or text/event-stream' },
|
|
260
|
+
id: null
|
|
261
|
+
}));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
let body = '';
|
|
265
|
+
req.on('data', chunk => (body += chunk));
|
|
266
|
+
req.on('end', async () => {
|
|
267
|
+
try {
|
|
268
|
+
const jsonData = JSON.parse(body);
|
|
269
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
270
|
+
if (sessionId && !/^[\x21-\x7E]+$/.test(sessionId)) {
|
|
271
|
+
logger.warn(`Invalid session ID format: ${sessionId}`);
|
|
272
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
273
|
+
res.end(JSON.stringify({
|
|
274
|
+
jsonrpc: '2.0',
|
|
275
|
+
error: { code: -32000, message: 'Bad Request: Session ID must contain only visible ASCII characters' },
|
|
276
|
+
id: jsonData.id || null
|
|
277
|
+
}));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
let transport;
|
|
281
|
+
if (sessionId && transports[sessionId]) {
|
|
282
|
+
transport = transports[sessionId];
|
|
283
|
+
}
|
|
284
|
+
else if (!sessionId) {
|
|
285
|
+
const newSessionId = randomUUID();
|
|
286
|
+
const isInit = isInitializeRequest(jsonData);
|
|
287
|
+
if (isInit) {
|
|
288
|
+
logger.info(`Creating new session for initialize request: ${newSessionId}`);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
logger.warn(`Creating fallback session for non-initialize request: ${newSessionId}`);
|
|
292
|
+
}
|
|
293
|
+
transport = new StreamableHTTPServerTransport({
|
|
294
|
+
sessionIdGenerator: () => newSessionId,
|
|
295
|
+
onsessioninitialized: (id) => {
|
|
296
|
+
transports[id] = transport;
|
|
297
|
+
logger.info(`Session initialized: ${id}`);
|
|
298
|
+
res.setHeader('mcp-session-id', id);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
transport.onclose = () => {
|
|
302
|
+
const sid = transport.sessionId;
|
|
303
|
+
if (sid && transports[sid]) {
|
|
304
|
+
delete transports[sid];
|
|
305
|
+
logger.info(`Session closed: ${sid}`);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
await server.connect(transport);
|
|
309
|
+
await transport.handleRequest(req, res, jsonData);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
logger.error(`Invalid session ID: ${sessionId}. Active sessions count: ${Object.keys(transports).length}`);
|
|
314
|
+
res.writeHead(400);
|
|
315
|
+
res.end(JSON.stringify({
|
|
316
|
+
jsonrpc: '2.0',
|
|
317
|
+
error: {
|
|
318
|
+
code: -32000,
|
|
319
|
+
message: 'Bad Request: Invalid session ID. Please initialize a new session first.'
|
|
320
|
+
},
|
|
321
|
+
id: jsonData.id || null
|
|
322
|
+
}));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await transport.handleRequest(req, res, jsonData);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
logger.error(`Error processing POST request: ${error}`);
|
|
329
|
+
if (!res.headersSent) {
|
|
330
|
+
res.writeHead(500);
|
|
331
|
+
res.end(JSON.stringify({
|
|
332
|
+
jsonrpc: '2.0',
|
|
333
|
+
error: { code: -32603, message: 'Internal server error' },
|
|
334
|
+
id: null
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else if (req.method === 'GET') {
|
|
341
|
+
const acceptHeader = req.headers.accept;
|
|
342
|
+
if (!acceptHeader || !acceptHeader.includes('text/event-stream')) {
|
|
343
|
+
logger.warn(`GET request without text/event-stream Accept header: ${acceptHeader}`);
|
|
344
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
345
|
+
res.end(JSON.stringify({
|
|
346
|
+
jsonrpc: '2.0',
|
|
347
|
+
error: { code: -32000, message: 'Method Not Allowed: GET requires Accept: text/event-stream' },
|
|
348
|
+
id: null
|
|
349
|
+
}));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
353
|
+
if (sessionId && !/^[\x21-\x7E]+$/.test(sessionId)) {
|
|
354
|
+
logger.warn(`Invalid session ID format in GET request: ${sessionId}`);
|
|
355
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
356
|
+
res.end(JSON.stringify({
|
|
357
|
+
jsonrpc: '2.0',
|
|
358
|
+
error: { code: -32000, message: 'Bad Request: Session ID must contain only visible ASCII characters' },
|
|
359
|
+
id: null
|
|
360
|
+
}));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (!sessionId || !transports[sessionId]) {
|
|
364
|
+
logger.warn(`SSE request with invalid session ID: ${sessionId}`);
|
|
365
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
366
|
+
res.end(JSON.stringify({
|
|
367
|
+
jsonrpc: '2.0',
|
|
368
|
+
error: { code: -32000, message: 'Bad Request: Invalid or missing session ID for SSE stream' },
|
|
369
|
+
id: null
|
|
370
|
+
}));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const lastEventId = req.headers['last-event-id'];
|
|
374
|
+
if (lastEventId) {
|
|
375
|
+
logger.info(`SSE resumability requested with Last-Event-ID: ${lastEventId}`);
|
|
376
|
+
}
|
|
377
|
+
logger.info(`Opening SSE stream for session: ${sessionId}`);
|
|
378
|
+
req.socket?.setTimeout(0);
|
|
379
|
+
req.socket?.setKeepAlive(true, 30000);
|
|
380
|
+
let streamClosed = false;
|
|
381
|
+
req.on('close', () => {
|
|
382
|
+
streamClosed = true;
|
|
383
|
+
logger.info(`Client disconnected SSE stream for session: ${sessionId}`);
|
|
384
|
+
});
|
|
385
|
+
req.on('aborted', () => {
|
|
386
|
+
streamClosed = true;
|
|
387
|
+
logger.info(`Client aborted SSE stream for session: ${sessionId}`);
|
|
388
|
+
});
|
|
389
|
+
const transport = transports[sessionId];
|
|
390
|
+
try {
|
|
391
|
+
await transport.handleRequest(req, res);
|
|
392
|
+
if (!streamClosed && !res.destroyed) {
|
|
393
|
+
logger.info(`Transport completed, maintaining SSE stream for session: ${sessionId}`);
|
|
394
|
+
const heartbeat = setInterval(() => {
|
|
395
|
+
if (streamClosed || res.destroyed) {
|
|
396
|
+
clearInterval(heartbeat);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
res.write(': heartbeat\n\n');
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
logger.error(`Heartbeat error for session ${sessionId}:`, error);
|
|
404
|
+
clearInterval(heartbeat);
|
|
405
|
+
}
|
|
406
|
+
}, 30000);
|
|
407
|
+
req.on('close', () => clearInterval(heartbeat));
|
|
408
|
+
res.on('close', () => clearInterval(heartbeat));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
logger.error(`SSE transport error for session ${sessionId}:`, error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else if (req.method === 'DELETE') {
|
|
416
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
417
|
+
if (!sessionId || !transports[sessionId]) {
|
|
418
|
+
res.writeHead(400);
|
|
419
|
+
res.end('Invalid or missing session ID');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const transport = transports[sessionId];
|
|
423
|
+
await transport.handleRequest(req, res);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
res.writeHead(405);
|
|
427
|
+
res.end('Method not allowed');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
res.writeHead(404);
|
|
432
|
+
res.end('Not found');
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
httpServer.listen(port, () => {
|
|
436
|
+
logger.info(`Socket MCP HTTP server version ${VERSION} started successfully on port ${port}`);
|
|
437
|
+
logger.info(`Connect to: http://localhost:${port}/`);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
logger.info('Starting in stdio mode');
|
|
442
|
+
const transport = new StdioServerTransport();
|
|
443
|
+
server.connect(transport)
|
|
444
|
+
.then(() => {
|
|
445
|
+
logger.info(`Socket MCP server version ${VERSION} started successfully`);
|
|
446
|
+
})
|
|
447
|
+
.catch((error) => {
|
|
448
|
+
logger.error(`Failed to start Socket MCP server: ${error.message}`);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"debug-client.d.ts","sourceRoot":"","sources":["debug-client.ts"],"names":[],"mappings":""}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node --experimental-strip-types
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
-
|
|
4
|
+
import { join } from 'path';
|
|
5
5
|
class SimpleJSONRPCClient {
|
|
6
6
|
process;
|
|
7
7
|
rl;
|
|
@@ -60,17 +60,18 @@ class SimpleJSONRPCClient {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
async function main() {
|
|
63
|
-
const apiKey = process.env
|
|
63
|
+
const apiKey = process.env['SOCKET_API_KEY'];
|
|
64
64
|
if (!apiKey) {
|
|
65
65
|
console.error('Error: SOCKET_API_KEY environment variable is required');
|
|
66
66
|
process.exit(1);
|
|
67
67
|
}
|
|
68
68
|
console.log('Starting MCP server debug client...');
|
|
69
|
-
const
|
|
69
|
+
const serverPath = join(import.meta.dirname, '..', 'index.ts');
|
|
70
|
+
console.log(`Using server script: ${serverPath}`);
|
|
71
|
+
const client = new SimpleJSONRPCClient('node', ['--experimental-strip-types', serverPath], {
|
|
70
72
|
SOCKET_API_KEY: apiKey
|
|
71
73
|
});
|
|
72
74
|
try {
|
|
73
|
-
// Initialize the connection
|
|
74
75
|
console.log('\n1. Initializing connection...');
|
|
75
76
|
const initResult = await client.sendRequest('initialize', {
|
|
76
77
|
protocolVersion: '0.1.0',
|
|
@@ -81,11 +82,9 @@ async function main() {
|
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
console.log('Initialize response:', JSON.stringify(initResult, null, 2));
|
|
84
|
-
// List available tools
|
|
85
85
|
console.log('\n2. Listing available tools...');
|
|
86
86
|
const toolsResult = await client.sendRequest('tools/list', {});
|
|
87
87
|
console.log('Available tools:', JSON.stringify(toolsResult, null, 2));
|
|
88
|
-
// Call the depscore tool
|
|
89
88
|
console.log('\n3. Calling depscore tool...');
|
|
90
89
|
const depscoreResult = await client.sendRequest('tools/call', {
|
|
91
90
|
name: 'depscore',
|
|
@@ -100,7 +99,6 @@ async function main() {
|
|
|
100
99
|
}
|
|
101
100
|
});
|
|
102
101
|
console.log('Depscore result:', JSON.stringify(depscoreResult, null, 2));
|
|
103
|
-
// Test with minimal input
|
|
104
102
|
console.log('\n4. Testing with minimal input (default to npm)...');
|
|
105
103
|
const minimalResult = await client.sendRequest('tools/call', {
|
|
106
104
|
name: 'depscore',
|
|
@@ -112,7 +110,6 @@ async function main() {
|
|
|
112
110
|
}
|
|
113
111
|
});
|
|
114
112
|
console.log('Minimal input result:', JSON.stringify(minimalResult, null, 2));
|
|
115
|
-
// Test error handling
|
|
116
113
|
console.log('\n5. Testing error handling (empty packages)...');
|
|
117
114
|
try {
|
|
118
115
|
await client.sendRequest('tools/call', {
|
|
@@ -135,4 +132,3 @@ async function main() {
|
|
|
135
132
|
}
|
|
136
133
|
}
|
|
137
134
|
main().catch(console.error);
|
|
138
|
-
//# sourceMappingURL=debug-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["http-client.ts"],"names":[],"mappings":""}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
// Helper function to parse SSE or JSON response
|
|
1
|
+
#!/usr/bin/env node --experimental-strip-types
|
|
2
|
+
import { join } from 'path';
|
|
4
3
|
async function parseResponse(response) {
|
|
5
4
|
const contentType = response.headers.get('content-type');
|
|
6
5
|
const text = await response.text();
|
|
7
6
|
if (contentType?.includes('text/event-stream')) {
|
|
8
|
-
// Parse SSE format: "event: message\ndata: {json}\n"
|
|
9
7
|
const dataMatch = text.match(/data: (.+)/);
|
|
10
8
|
if (dataMatch) {
|
|
11
9
|
return JSON.parse(dataMatch[1]);
|
|
@@ -16,15 +14,13 @@ async function parseResponse(response) {
|
|
|
16
14
|
return JSON.parse(text);
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
|
-
// Simple HTTP client for testing MCP server in HTTP mode
|
|
20
17
|
async function testHTTPMode() {
|
|
21
|
-
const baseUrl = (process.env
|
|
18
|
+
const baseUrl = (process.env['MCP_URL'] || 'http://localhost:3000').replace(/\/$/, '');
|
|
22
19
|
const sessionId = `test-session-${Date.now()}`;
|
|
23
20
|
console.log('Testing Socket MCP in HTTP mode...');
|
|
24
21
|
console.log(`Server URL: ${baseUrl}`);
|
|
25
22
|
console.log(`Session ID: ${sessionId}`);
|
|
26
23
|
try {
|
|
27
|
-
// 1. Initialize connection
|
|
28
24
|
console.log('\n1. Initializing connection...');
|
|
29
25
|
const initRequest = {
|
|
30
26
|
jsonrpc: '2.0',
|
|
@@ -43,18 +39,16 @@ async function testHTTPMode() {
|
|
|
43
39
|
method: 'POST',
|
|
44
40
|
headers: {
|
|
45
41
|
'Content-Type': 'application/json',
|
|
46
|
-
|
|
42
|
+
Accept: 'application/json, text/event-stream',
|
|
47
43
|
'User-Agent': 'socket-mcp-debug-client/1.0.0'
|
|
48
44
|
},
|
|
49
45
|
body: JSON.stringify(initRequest)
|
|
50
46
|
});
|
|
51
47
|
const initResult = await parseResponse(initResponse);
|
|
52
48
|
console.log('Initialize response:', JSON.stringify(initResult, null, 2));
|
|
53
|
-
// Extract session ID from response headers
|
|
54
49
|
const serverSessionId = initResponse.headers.get('mcp-session-id');
|
|
55
50
|
const actualSessionId = serverSessionId || sessionId;
|
|
56
51
|
console.log('Session ID:', actualSessionId);
|
|
57
|
-
// 2. List tools
|
|
58
52
|
console.log('\n2. Listing available tools...');
|
|
59
53
|
const toolsRequest = {
|
|
60
54
|
jsonrpc: '2.0',
|
|
@@ -66,14 +60,13 @@ async function testHTTPMode() {
|
|
|
66
60
|
method: 'POST',
|
|
67
61
|
headers: {
|
|
68
62
|
'Content-Type': 'application/json',
|
|
69
|
-
|
|
63
|
+
Accept: 'application/json, text/event-stream',
|
|
70
64
|
'mcp-session-id': actualSessionId
|
|
71
65
|
},
|
|
72
66
|
body: JSON.stringify(toolsRequest)
|
|
73
67
|
});
|
|
74
68
|
const toolsResult = await parseResponse(toolsResponse);
|
|
75
69
|
console.log('Available tools:', JSON.stringify(toolsResult, null, 2));
|
|
76
|
-
// 3. Call depscore
|
|
77
70
|
console.log('\n3. Calling depscore tool...');
|
|
78
71
|
const depscoreRequest = {
|
|
79
72
|
jsonrpc: '2.0',
|
|
@@ -94,27 +87,24 @@ async function testHTTPMode() {
|
|
|
94
87
|
method: 'POST',
|
|
95
88
|
headers: {
|
|
96
89
|
'Content-Type': 'application/json',
|
|
97
|
-
|
|
90
|
+
Accept: 'application/json, text/event-stream',
|
|
98
91
|
'mcp-session-id': actualSessionId
|
|
99
92
|
},
|
|
100
93
|
body: JSON.stringify(depscoreRequest)
|
|
101
94
|
});
|
|
102
95
|
const depscoreResult = await parseResponse(depscoreResponse);
|
|
103
96
|
console.log('Depscore result:', JSON.stringify(depscoreResult, null, 2));
|
|
104
|
-
// 4. Test SSE stream (optional)
|
|
105
97
|
console.log('\n4. Testing SSE stream connection...');
|
|
106
98
|
const sseResponse = await fetch(`${baseUrl}/`, {
|
|
107
99
|
method: 'GET',
|
|
108
100
|
headers: {
|
|
109
101
|
'mcp-session-id': actualSessionId,
|
|
110
|
-
|
|
102
|
+
Accept: 'text/event-stream'
|
|
111
103
|
}
|
|
112
104
|
});
|
|
113
105
|
if (sseResponse.ok) {
|
|
114
106
|
console.log('SSE stream connected successfully');
|
|
115
|
-
// Note: In a real implementation, you'd parse the SSE stream
|
|
116
107
|
}
|
|
117
|
-
// 5. Clean up session
|
|
118
108
|
console.log('\n5. Cleaning up session...');
|
|
119
109
|
const cleanupResponse = await fetch(`${baseUrl}/`, {
|
|
120
110
|
method: 'DELETE',
|
|
@@ -128,26 +118,24 @@ async function testHTTPMode() {
|
|
|
128
118
|
console.error('Error:', error);
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
|
-
// Usage instructions
|
|
132
121
|
if (process.argv.includes('--help')) {
|
|
122
|
+
const serverScript = join(import.meta.dirname, '..', 'index.ts');
|
|
133
123
|
console.log(`
|
|
134
124
|
Socket MCP HTTP Client Debugger
|
|
135
125
|
|
|
136
126
|
Usage:
|
|
137
127
|
# Start the MCP server in HTTP mode first:
|
|
138
|
-
MCP_HTTP_MODE=true SOCKET_API_KEY=your-api-key
|
|
128
|
+
MCP_HTTP_MODE=true SOCKET_API_KEY=your-api-key node --experimental-strip-types ${serverScript}
|
|
139
129
|
|
|
140
130
|
# Then run this client:
|
|
141
|
-
|
|
142
|
-
node ./build/http-client.js
|
|
131
|
+
node --experimental-strip-types ./mock-client/http-client.ts
|
|
143
132
|
|
|
144
133
|
Environment variables:
|
|
145
134
|
MCP_URL - Server URL (default: http://localhost:3000)
|
|
146
135
|
|
|
147
136
|
Example:
|
|
148
|
-
MCP_URL=http://localhost:8080 node ./
|
|
137
|
+
MCP_URL=http://localhost:8080 node --experimental-strip-types ./mock-client/http-client.ts
|
|
149
138
|
`);
|
|
150
139
|
process.exit(0);
|
|
151
140
|
}
|
|
152
141
|
testHTTPMode().catch(console.error);
|
|
153
|
-
//# sourceMappingURL=http-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-client.d.ts","sourceRoot":"","sources":["stdio-client.ts"],"names":[],"mappings":""}
|