@socketsecurity/mcp 0.0.16 → 0.0.17

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.
Files changed (2) hide show
  1. package/index.js +236 -129
  2. package/package.json +13 -3
package/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
6
+ import { randomUUID } from 'node:crypto';
5
7
  import { z } from 'zod';
6
8
  import pino from 'pino';
7
9
  import readline from 'readline';
@@ -58,75 +60,92 @@ function buildSocketHeaders() {
58
60
  authorization: `Bearer ${SOCKET_API_KEY}`
59
61
  };
60
62
  }
61
- const server = new McpServer({
62
- name: 'socket',
63
- version: VERSION,
64
- });
65
- server.registerTool('depscore', {
66
- title: 'Dependency Score Tool',
67
- description: "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).",
68
- inputSchema: z.object({
69
- packages: z.array(z.object({
70
- ecosystem: z.string().describe('The package ecosystem (e.g., npm, pypi)').default('npm'),
71
- depname: z.string().describe('The name of the dependency'),
72
- version: z.string().describe("The version of the dependency, use 'unknown' if not known").default('unknown'),
73
- })).describe('Array of packages to check'),
74
- }),
75
- annotations: {
76
- readOnlyHint: true,
77
- },
78
- }, async ({ packages }) => {
79
- logger.info(`Received request for ${packages.length} packages`);
80
- const components = packages.map(pkg => {
81
- const cleanedVersion = (pkg.version ?? 'unknown').replace(/[\^~]/g, '');
82
- const ecosystem = pkg.ecosystem ?? 'npm';
83
- let purl;
84
- if (cleanedVersion === '1.0.0' || cleanedVersion === 'unknown' || !cleanedVersion) {
85
- purl = `pkg:${ecosystem}/${pkg.depname}`;
86
- }
87
- else {
88
- logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`);
89
- purl = `pkg:${ecosystem}/${pkg.depname}@${cleanedVersion}`;
90
- }
91
- return { purl };
92
- });
93
- try {
94
- const response = await fetch(SOCKET_API_URL, {
95
- method: 'POST',
96
- headers: buildSocketHeaders(),
97
- body: JSON.stringify({ components })
63
+ function createConfiguredServer() {
64
+ const srv = new McpServer({ name: 'socket', version: VERSION });
65
+ srv.registerTool('depscore', {
66
+ title: 'Dependency Score Tool',
67
+ description: "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).",
68
+ inputSchema: {
69
+ packages: z.array(z.object({
70
+ ecosystem: z.string().describe('The package ecosystem (e.g., npm, pypi)').default('npm'),
71
+ depname: z.string().describe('The name of the dependency'),
72
+ version: z.string().describe("The version of the dependency, use 'unknown' if not known").default('unknown'),
73
+ })).describe('Array of packages to check'),
74
+ },
75
+ annotations: {
76
+ readOnlyHint: true,
77
+ },
78
+ }, async ({ packages }) => {
79
+ logger.info(`Received request for ${packages.length} packages`);
80
+ const components = packages.map((pkg) => {
81
+ const cleanedVersion = (pkg.version ?? 'unknown').replace(/[\^~]/g, '');
82
+ const ecosystem = pkg.ecosystem ?? 'npm';
83
+ let purl;
84
+ if (cleanedVersion === '1.0.0' || cleanedVersion === 'unknown' || !cleanedVersion) {
85
+ purl = `pkg:${ecosystem}/${pkg.depname}`;
86
+ }
87
+ else {
88
+ logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`);
89
+ purl = `pkg:${ecosystem}/${pkg.depname}@${cleanedVersion}`;
90
+ }
91
+ return { purl };
98
92
  });
99
- const responseText = await response.text();
100
- if (response.status !== 200) {
101
- const errorMsg = `Error processing packages: [${response.status}] ${responseText}`;
102
- logger.error(errorMsg);
103
- return {
104
- content: [{ type: 'text', text: errorMsg }],
105
- isError: true
106
- };
107
- }
108
- else if (!responseText.trim()) {
109
- const errorMsg = 'No packages were found.';
110
- logger.error(errorMsg);
111
- return {
112
- content: [{ type: 'text', text: errorMsg }],
113
- isError: true
114
- };
115
- }
116
93
  try {
117
- const results = [];
118
- if ((response.headers.get('content-type') || '').includes('x-ndjson')) {
119
- const jsonLines = responseText.split('\n')
120
- .filter(line => line.trim())
121
- .map(line => JSON.parse(line));
122
- if (!jsonLines.length) {
123
- const errorMsg = 'No valid JSON objects found in NDJSON response';
124
- return {
125
- content: [{ type: 'text', text: errorMsg }],
126
- isError: true
127
- };
94
+ const response = await fetch(SOCKET_API_URL, {
95
+ method: 'POST',
96
+ headers: buildSocketHeaders(),
97
+ body: JSON.stringify({ components })
98
+ });
99
+ const responseText = await response.text();
100
+ if (response.status !== 200) {
101
+ const errorMsg = `Error processing packages: [${response.status}] ${responseText}`;
102
+ logger.error(errorMsg);
103
+ return {
104
+ content: [{ type: 'text', text: errorMsg }],
105
+ isError: true
106
+ };
107
+ }
108
+ else if (!responseText.trim()) {
109
+ const errorMsg = 'No packages were found.';
110
+ logger.error(errorMsg);
111
+ return {
112
+ content: [{ type: 'text', text: errorMsg }],
113
+ isError: true
114
+ };
115
+ }
116
+ try {
117
+ const results = [];
118
+ if ((response.headers.get('content-type') || '').includes('x-ndjson')) {
119
+ const jsonLines = responseText.split('\n')
120
+ .filter(line => line.trim())
121
+ .map(line => JSON.parse(line));
122
+ if (!jsonLines.length) {
123
+ const errorMsg = 'No valid JSON objects found in NDJSON response';
124
+ return {
125
+ content: [{ type: 'text', text: errorMsg }],
126
+ isError: true
127
+ };
128
+ }
129
+ for (const jsonData of jsonLines) {
130
+ const purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
131
+ if (jsonData.score && jsonData.score.overall !== undefined) {
132
+ const scoreEntries = Object.entries(jsonData.score)
133
+ .filter(([key]) => key !== 'overall' && key !== 'uuid')
134
+ .map(([key, value]) => {
135
+ const numValue = Number(value);
136
+ const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue;
137
+ return `${key}: ${displayValue}`;
138
+ })
139
+ .join(', ');
140
+ results.push(`${purl}: ${scoreEntries}`);
141
+ }
142
+ else {
143
+ results.push(`${purl}: No score found`);
144
+ }
145
+ }
128
146
  }
129
- for (const jsonData of jsonLines) {
147
+ else {
148
+ const jsonData = JSON.parse(responseText);
130
149
  const purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
131
150
  if (jsonData.score && jsonData.score.overall !== undefined) {
132
151
  const scoreEntries = Object.entries(jsonData.score)
@@ -143,56 +162,39 @@ server.registerTool('depscore', {
143
162
  results.push(`${purl}: No score found`);
144
163
  }
145
164
  }
165
+ return {
166
+ content: [
167
+ {
168
+ type: 'text',
169
+ text: results.length > 0
170
+ ? `Dependency scores:\n${results.join('\n')}`
171
+ : 'No scores found for the provided packages'
172
+ }
173
+ ]
174
+ };
146
175
  }
147
- else {
148
- const jsonData = JSON.parse(responseText);
149
- const purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
150
- if (jsonData.score && jsonData.score.overall !== undefined) {
151
- const scoreEntries = Object.entries(jsonData.score)
152
- .filter(([key]) => key !== 'overall' && key !== 'uuid')
153
- .map(([key, value]) => {
154
- const numValue = Number(value);
155
- const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue;
156
- return `${key}: ${displayValue}`;
157
- })
158
- .join(', ');
159
- results.push(`${purl}: ${scoreEntries}`);
160
- }
161
- else {
162
- results.push(`${purl}: No score found`);
163
- }
176
+ catch (e) {
177
+ const error = e;
178
+ const errorMsg = `JSON parsing error: ${error.message} -- Response: ${responseText}`;
179
+ logger.error(errorMsg);
180
+ return {
181
+ content: [{ type: 'text', text: 'Error parsing response from Socket API' }],
182
+ isError: true
183
+ };
164
184
  }
165
- return {
166
- content: [
167
- {
168
- type: 'text',
169
- text: results.length > 0
170
- ? `Dependency scores:\n${results.join('\n')}`
171
- : 'No scores found for the provided packages'
172
- }
173
- ]
174
- };
175
185
  }
176
186
  catch (e) {
177
187
  const error = e;
178
- const errorMsg = `JSON parsing error: ${error.message} -- Response: ${responseText}`;
188
+ const errorMsg = `Error processing packages: ${error.message}`;
179
189
  logger.error(errorMsg);
180
190
  return {
181
- content: [{ type: 'text', text: 'Error parsing response from Socket API' }],
191
+ content: [{ type: 'text', text: 'Error connecting to Socket API' }],
182
192
  isError: true
183
193
  };
184
194
  }
185
- }
186
- catch (e) {
187
- const error = e;
188
- const errorMsg = `Error processing packages: ${error.message}`;
189
- logger.error(errorMsg);
190
- return {
191
- content: [{ type: 'text', text: 'Error connecting to Socket API' }],
192
- isError: true
193
- };
194
- }
195
- });
195
+ });
196
+ return srv;
197
+ }
196
198
  const useHttp = process.env['MCP_HTTP_MODE'] === 'true' || process.argv.includes('--http');
197
199
  const port = parseInt(process.env['MCP_PORT'] || '3000', 10);
198
200
  if (!SOCKET_API_KEY) {
@@ -208,7 +210,30 @@ if (!SOCKET_API_KEY) {
208
210
  }
209
211
  if (useHttp) {
210
212
  logger.info(`Starting HTTP server on port ${port}`);
211
- let httpTransport = null;
213
+ const sessions = new Map();
214
+ function destroySession(id) {
215
+ const s = sessions.get(id);
216
+ if (!s)
217
+ return;
218
+ sessions.delete(id);
219
+ try {
220
+ s.transport.close();
221
+ }
222
+ catch { }
223
+ s.server.close().catch(() => { });
224
+ logger.info(`Session ${id} destroyed`);
225
+ }
226
+ const SESSION_TTL_MS = 30 * 60 * 1000;
227
+ const reapInterval = setInterval(() => {
228
+ const now = Date.now();
229
+ for (const [id, session] of sessions.entries()) {
230
+ if (now - session.lastActivity > SESSION_TTL_MS) {
231
+ logger.info(`Reaping idle session ${id}`);
232
+ destroySession(id);
233
+ }
234
+ }
235
+ }, 60_000);
236
+ reapInterval.unref();
212
237
  const httpServer = createServer(async (req, res) => {
213
238
  let url;
214
239
  try {
@@ -270,8 +295,9 @@ if (useHttp) {
270
295
  }
271
296
  if (origin) {
272
297
  res.setHeader('Access-Control-Allow-Origin', origin);
273
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
274
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept');
298
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
299
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id');
300
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
275
301
  }
276
302
  if (req.method === 'OPTIONS') {
277
303
  res.writeHead(200);
@@ -279,37 +305,62 @@ if (useHttp) {
279
305
  return;
280
306
  }
281
307
  if (url.pathname === '/') {
308
+ const accept = req.headers.accept || '';
309
+ if (!accept.includes('application/json') || !accept.includes('text/event-stream')) {
310
+ const requiredAccept = 'application/json, text/event-stream';
311
+ req.headers.accept = requiredAccept;
312
+ const idx = req.rawHeaders.findIndex(h => h.toLowerCase() === 'accept');
313
+ if (idx !== -1) {
314
+ req.rawHeaders[idx + 1] = requiredAccept;
315
+ }
316
+ else {
317
+ req.rawHeaders.push('Accept', requiredAccept);
318
+ }
319
+ }
282
320
  if (req.method === 'POST') {
283
321
  let body = '';
284
- req.on('data', chunk => (body += chunk));
322
+ req.on('data', (chunk) => { body += chunk; });
285
323
  req.on('end', async () => {
286
324
  try {
287
325
  const jsonData = JSON.parse(body);
288
- if (jsonData && jsonData.method === 'initialize') {
326
+ const sessionId = req.headers['mcp-session-id'] || undefined;
327
+ const session = sessionId ? sessions.get(sessionId) : undefined;
328
+ let transport = session?.transport;
329
+ if (!transport && isInitializeRequest(jsonData)) {
289
330
  const clientInfo = jsonData.params?.clientInfo;
290
331
  logger.info(`Client connected: ${clientInfo?.name || 'unknown'} v${clientInfo?.version || 'unknown'} from ${origin || host}`);
291
- if (httpTransport) {
292
- try {
293
- httpTransport.close();
294
- }
295
- catch { }
296
- }
297
- httpTransport = new StreamableHTTPServerTransport({
298
- sessionIdGenerator: undefined,
299
- enableJsonResponse: true
332
+ const server = createConfiguredServer();
333
+ const newTransport = new StreamableHTTPServerTransport({
334
+ enableJsonResponse: true,
335
+ sessionIdGenerator: () => randomUUID(),
336
+ onsessioninitialized: (id) => {
337
+ sessions.set(id, { transport: newTransport, server, lastActivity: Date.now() });
338
+ },
339
+ onsessionclosed: (id) => { destroySession(id); }
300
340
  });
301
- await server.connect(httpTransport);
302
- await httpTransport.handleRequest(req, res, jsonData);
341
+ newTransport.onclose = () => {
342
+ const id = newTransport.sessionId;
343
+ if (id)
344
+ destroySession(id);
345
+ };
346
+ transport = newTransport;
347
+ await server.connect(transport);
348
+ }
349
+ if (!transport) {
350
+ res.writeHead(400, { 'Content-Type': 'application/json' });
351
+ res.end(JSON.stringify({
352
+ jsonrpc: '2.0',
353
+ error: { code: -32000, message: 'Bad Request: No valid session. Send initialize first.' },
354
+ id: null
355
+ }));
303
356
  return;
304
357
  }
305
- if (!httpTransport) {
306
- httpTransport = new StreamableHTTPServerTransport({
307
- sessionIdGenerator: undefined,
308
- enableJsonResponse: true
309
- });
310
- await server.connect(httpTransport);
358
+ if (sessionId) {
359
+ const activeSession = sessions.get(sessionId);
360
+ if (activeSession)
361
+ activeSession.lastActivity = Date.now();
311
362
  }
312
- await httpTransport.handleRequest(req, res, jsonData);
363
+ await transport.handleRequest(req, res, jsonData);
313
364
  }
314
365
  catch (error) {
315
366
  logger.error(`Error processing POST request: ${error}`);
@@ -324,6 +375,61 @@ if (useHttp) {
324
375
  }
325
376
  });
326
377
  }
378
+ else if (req.method === 'GET') {
379
+ const sessionId = req.headers['mcp-session-id'] || undefined;
380
+ const session = sessionId ? sessions.get(sessionId) : undefined;
381
+ if (!session) {
382
+ res.writeHead(404, { 'Content-Type': 'application/json' });
383
+ res.end(JSON.stringify({
384
+ jsonrpc: '2.0',
385
+ error: { code: -32000, message: 'Not Found: Invalid or expired session. Re-initialize.' },
386
+ id: null
387
+ }));
388
+ return;
389
+ }
390
+ try {
391
+ session.lastActivity = Date.now();
392
+ await session.transport.handleRequest(req, res);
393
+ }
394
+ catch (error) {
395
+ logger.error(`Error processing GET request: ${error}`);
396
+ if (!res.headersSent) {
397
+ res.writeHead(500);
398
+ res.end(JSON.stringify({
399
+ jsonrpc: '2.0',
400
+ error: { code: -32603, message: 'Internal server error' },
401
+ id: null
402
+ }));
403
+ }
404
+ }
405
+ }
406
+ else if (req.method === 'DELETE') {
407
+ const sessionId = req.headers['mcp-session-id'] || undefined;
408
+ const transport = sessionId ? sessions.get(sessionId)?.transport : undefined;
409
+ if (!transport) {
410
+ res.writeHead(404, { 'Content-Type': 'application/json' });
411
+ res.end(JSON.stringify({
412
+ jsonrpc: '2.0',
413
+ error: { code: -32000, message: 'Not Found: Invalid or expired session.' },
414
+ id: null
415
+ }));
416
+ return;
417
+ }
418
+ try {
419
+ await transport.handleRequest(req, res);
420
+ }
421
+ catch (error) {
422
+ logger.error(`Error processing DELETE request: ${error}`);
423
+ if (!res.headersSent) {
424
+ res.writeHead(500);
425
+ res.end(JSON.stringify({
426
+ jsonrpc: '2.0',
427
+ error: { code: -32603, message: 'Internal server error' },
428
+ id: null
429
+ }));
430
+ }
431
+ }
432
+ }
327
433
  else {
328
434
  res.writeHead(405);
329
435
  res.end('Method not allowed');
@@ -341,6 +447,7 @@ if (useHttp) {
341
447
  }
342
448
  else {
343
449
  logger.info('Starting in stdio mode');
450
+ const server = createConfiguredServer();
344
451
  const transport = new StdioServerTransport();
345
452
  server.connect(transport)
346
453
  .then(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@socketsecurity/mcp",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -50,11 +50,21 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@anthropic-ai/mcpb": "^1.1.0",
53
- "@modelcontextprotocol/sdk": "^1.18.0",
53
+ "@modelcontextprotocol/sdk": "1.26.0",
54
54
  "pino": "^10.0.0",
55
55
  "pino-pretty": "^13.0.0",
56
56
  "semver": "^7.7.2",
57
- "zod": "^3.24.4"
57
+ "zod": "3.25.76"
58
+ },
59
+ "overrides": {
60
+ "zod": "3.25.76",
61
+ "zod-to-json-schema": "3.25.1"
62
+ },
63
+ "pnpm": {
64
+ "overrides": {
65
+ "zod": "3.25.76",
66
+ "zod-to-json-schema": "3.25.1"
67
+ }
58
68
  },
59
69
  "devDependencies": {
60
70
  "@types/node": "^24.0.7",