@socketsecurity/mcp 0.0.2 → 0.0.3

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 (3) hide show
  1. package/README.md +1 -1
  2. package/build/index.js +269 -86
  3. package/package.json +13 -4
package/README.md CHANGED
@@ -20,7 +20,7 @@ The `depscore` tool allows AI assistants to query the Socket API for dependency
20
20
 
21
21
  ### Getting an API key
22
22
 
23
- To use the Socket MCP Server, you need to create an API key. You can do this by following [these steps](https://docs.socket.dev/reference/creating-and-managing-api-tokens).
23
+ To use the Socket MCP Server, you need to create an API key. You can do this by following [these steps](https://docs.socket.dev/reference/creating-and-managing-api-tokens). The only required permission scope is `packages:list`, which allows the MCP server to query package metadata for dependency scores.
24
24
 
25
25
 
26
26
  ### Usage with Claude Desktop
package/build/index.js CHANGED
@@ -1,31 +1,65 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
5
  import { z } from "zod";
5
- import fetch from 'node-fetch';
6
- import winston from 'winston';
7
- const VERSION = "0.0.1"; // Version of the MCP server
8
- // Configure winston logger
9
- const logger = winston.createLogger({
6
+ import { pino } from 'pino';
7
+ import readline from 'readline';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+ import { readFileSync } from 'fs';
11
+ import { createServer } from 'http';
12
+ import { randomUUID } from 'crypto';
13
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ // Extract version from package.json
17
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
18
+ const VERSION = packageJson.version || "0.0.1";
19
+ // Configure pino logger
20
+ const logger = pino({
10
21
  level: 'info',
11
- format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()),
12
- transports: [
13
- new winston.transports.File({ filename: 'socket-mcp-error.log', level: 'error' }),
14
- new winston.transports.File({ filename: 'socket-mcp.log' })
15
- ]
22
+ transport: {
23
+ targets: [
24
+ {
25
+ target: 'pino/file',
26
+ options: { destination: '/tmp/socket-mcp-error.log' },
27
+ level: 'error'
28
+ },
29
+ {
30
+ target: 'pino/file',
31
+ options: { destination: '/tmp/socket-mcp.log' },
32
+ level: 'info'
33
+ }
34
+ ]
35
+ }
16
36
  });
17
- const SOCKET_API_URL = "https://api.socket.dev/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false";
18
- const SOCKET_API_KEY = process.env.SOCKET_API_KEY || "";
19
- if (!SOCKET_API_KEY) {
20
- logger.error("SOCKET_API_KEY environment variable is not set");
21
- process.exit(1);
37
+ // Socket API URL - use localhost when debugging is enabled, otherwise use production
38
+ const SOCKET_API_URL = process.env.SOCKET_DEBUG === 'true'
39
+ ? "http://localhost:8866/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false"
40
+ : "https://api.socket.dev/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false";
41
+ // Function to get API key interactively (only for HTTP mode)
42
+ async function getApiKeyInteractively() {
43
+ const rl = readline.createInterface({
44
+ input: process.stdin,
45
+ output: process.stderr
46
+ });
47
+ const apiKey = await new Promise((resolve) => {
48
+ rl.question('Please enter your Socket API key: ', (answer) => {
49
+ rl.close();
50
+ resolve(answer);
51
+ });
52
+ });
53
+ if (!apiKey) {
54
+ logger.error("No API key provided");
55
+ process.exit(1);
56
+ }
57
+ return apiKey;
22
58
  }
23
- const SOCKET_HEADERS = {
24
- "user-agent": `socket-mcp/${VERSION}`,
25
- "accept": "application/x-ndjson",
26
- "content-type": "application/json",
27
- "authorization": `Bearer ${SOCKET_API_KEY}`
28
- };
59
+ // Initialize API key
60
+ let SOCKET_API_KEY = process.env.SOCKET_API_KEY || "";
61
+ // Transport management
62
+ const transports = {};
29
63
  // Create server instance
30
64
  const server = new McpServer({
31
65
  name: "socket",
@@ -34,34 +68,40 @@ const server = new McpServer({
34
68
  capabilities: {
35
69
  resources: {},
36
70
  tools: {},
71
+ streaming: {}
37
72
  },
38
73
  });
39
- server.tool("depscore", "Get the dependency score of a package 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).", {
40
- ecosystem: z.string().describe("The package ecosystem (e.g., npm, pypi)").default("npm"),
41
- depname: z.string().describe("The name of the dependency"),
42
- version: z.string().describe("The version of the dependency, use 'unknown' if not known").default("unknown"),
43
- }, async ({ ecosystem, depname, version }) => {
44
- logger.info(`Received request for ${depname} (${version}) in ${ecosystem} ecosystem`);
45
- // cleanup version
46
- let purl;
47
- const cleanedVersion = version.replace(/[\^~]/g, ''); // Remove ^ and ~ from version
48
- if (cleanedVersion === "1.0.0" || cleanedVersion === "unknown" || !cleanedVersion) {
49
- purl = `pkg:${ecosystem}/${depname}`;
50
- }
51
- else {
52
- logger.info(`Using version ${cleanedVersion} for ${depname}`);
53
- purl = `pkg:${ecosystem}/${depname}@${cleanedVersion}`;
54
- }
74
+ 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).", {
75
+ packages: z.array(z.object({
76
+ ecosystem: z.string().describe("The package ecosystem (e.g., npm, pypi)").default("npm"),
77
+ depname: z.string().describe("The name of the dependency"),
78
+ version: z.string().describe("The version of the dependency, use 'unknown' if not known").default("unknown"),
79
+ })).describe("Array of packages to check"),
80
+ }, async ({ packages }) => {
81
+ logger.info(`Received request for ${packages.length} packages`);
82
+ // Build components array for the API request
83
+ const components = packages.map(pkg => {
84
+ const cleanedVersion = pkg.version.replace(/[\^~]/g, ''); // Remove ^ and ~ from version
85
+ let purl;
86
+ if (cleanedVersion === "1.0.0" || cleanedVersion === "unknown" || !cleanedVersion) {
87
+ purl = `pkg:${pkg.ecosystem}/${pkg.depname}`;
88
+ }
89
+ else {
90
+ logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`);
91
+ purl = `pkg:${pkg.ecosystem}/${pkg.depname}@${cleanedVersion}`;
92
+ }
93
+ return { purl };
94
+ });
55
95
  try {
56
- // Make a POST request to the Socket API
96
+ // Make a POST request to the Socket API with all packages
57
97
  const response = await fetch(SOCKET_API_URL, {
58
98
  method: 'POST',
59
99
  headers: SOCKET_HEADERS,
60
- body: JSON.stringify({ components: [{ purl }] })
100
+ body: JSON.stringify({ components })
61
101
  });
62
102
  const responseText = await response.text();
63
103
  if (response.status !== 200) {
64
- const errorMsg = `Error processing ${purl}: [${response.status}] ${responseText}`;
104
+ const errorMsg = `Error processing packages: [${response.status}] ${responseText}`;
65
105
  logger.error(errorMsg);
66
106
  return {
67
107
  content: [{ type: "text", text: errorMsg }],
@@ -69,7 +109,7 @@ server.tool("depscore", "Get the dependency score of a package with the `depscor
69
109
  };
70
110
  }
71
111
  else if (!responseText.trim()) {
72
- const errorMsg = `${purl} was not found.`;
112
+ const errorMsg = `No packages were found.`;
73
113
  logger.error(errorMsg);
74
114
  return {
75
115
  content: [{ type: "text", text: errorMsg }],
@@ -78,78 +118,221 @@ server.tool("depscore", "Get the dependency score of a package with the `depscor
78
118
  }
79
119
  try {
80
120
  // Handle NDJSON (multiple JSON objects, one per line)
81
- let jsonData;
121
+ let results = [];
82
122
  if ((response.headers.get('content-type') || '').includes('x-ndjson')) {
83
123
  const jsonLines = responseText.split('\n')
84
124
  .filter(line => line.trim())
85
125
  .map(line => JSON.parse(line));
86
126
  if (!jsonLines.length) {
87
- const errorMsg = `No valid JSON objects found in NDJSON response for ${purl}`;
127
+ const errorMsg = `No valid JSON objects found in NDJSON response`;
88
128
  return {
89
129
  content: [{ type: "text", text: errorMsg }],
90
130
  isError: true
91
131
  };
92
132
  }
93
- jsonData = jsonLines[0];
94
- }
95
- else {
96
- jsonData = JSON.parse(responseText);
97
- }
98
- if (jsonData.score && jsonData.score.overall !== undefined) {
99
- // Unroll the jsonData.score object into key-value pairs
100
- const scoreEntries = Object.entries(jsonData.score)
101
- .filter(([key]) => key !== "overall" && key !== "uuid")
102
- .map(([key, value]) => `${key}: ${value}`)
103
- .join(', ');
104
- return {
105
- content: [
106
- {
107
- type: "text",
108
- text: `Dependency scores for ${purl}: ${scoreEntries}`
109
- }
110
- ]
111
- };
133
+ // Process each result
134
+ for (const jsonData of jsonLines) {
135
+ let purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
136
+ if (jsonData.score && jsonData.score.overall !== undefined) {
137
+ const scoreEntries = Object.entries(jsonData.score)
138
+ .filter(([key]) => key !== "overall" && key !== "uuid")
139
+ .map(([key, value]) => `${key}: ${value}`)
140
+ .join(', ');
141
+ results.push(`${purl}: ${scoreEntries}`);
142
+ }
143
+ else {
144
+ results.push(`${purl}: No score found`);
145
+ }
146
+ }
112
147
  }
113
148
  else {
114
- return {
115
- content: [
116
- {
117
- type: "text",
118
- text: `No score found for ${purl}`
119
- }
120
- ]
121
- };
149
+ const jsonData = JSON.parse(responseText);
150
+ let purl = `pkg:${jsonData.type || 'unknown'}/${jsonData.name || 'unknown'}@${jsonData.version || 'unknown'}`;
151
+ if (jsonData.score && jsonData.score.overall !== undefined) {
152
+ const scoreEntries = Object.entries(jsonData.score)
153
+ .filter(([key]) => key !== "overall" && key !== "uuid")
154
+ .map(([key, value]) => `${key}: ${value}`)
155
+ .join(', ');
156
+ results.push(`${purl}: ${scoreEntries}`);
157
+ }
122
158
  }
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: results.length > 0
164
+ ? `Dependency scores:\n${results.join('\n')}`
165
+ : "No scores found for the provided packages"
166
+ }
167
+ ]
168
+ };
123
169
  }
124
170
  catch (e) {
125
171
  const error = e;
126
- const errorMsg = `JSON parsing error for ${purl}: ${error.message} -- Response: ${responseText}`;
172
+ const errorMsg = `JSON parsing error: ${error.message} -- Response: ${responseText}`;
127
173
  logger.error(errorMsg);
128
- const llmResponse = `Package ${purl} not found.`;
129
174
  return {
130
- content: [{ type: "text", text: llmResponse }],
175
+ content: [{ type: "text", text: "Error parsing response from Socket API" }],
131
176
  isError: true
132
177
  };
133
178
  }
134
179
  }
135
180
  catch (e) {
136
181
  const error = e;
137
- const errorMsg = `Error processing ${purl}: ${error.message}`;
182
+ const errorMsg = `Error processing packages: ${error.message}`;
138
183
  logger.error(errorMsg);
139
- const llmResponse = `Package ${purl} not found.`;
140
184
  return {
141
- content: [{ type: "text", text: llmResponse }],
185
+ content: [{ type: "text", text: "Error connecting to Socket API" }],
142
186
  isError: true
143
187
  };
144
188
  }
145
189
  });
146
- // Create a stdio transport and start the server
147
- const transport = new StdioServerTransport();
148
- server.connect(transport)
149
- .then(() => {
150
- logger.info("Socket MCP server started successfully");
151
- })
152
- .catch((error) => {
153
- logger.error(`Failed to start Socket MCP server: ${error.message}`);
154
- process.exit(1);
155
- });
190
+ // Determine transport mode from environment or arguments
191
+ const useHttp = process.env.MCP_HTTP_MODE === 'true' || process.argv.includes('--http');
192
+ const port = parseInt(process.env.MCP_PORT || '3000', 10);
193
+ // Validate API key - in stdio mode, we can't prompt interactively
194
+ if (!SOCKET_API_KEY) {
195
+ if (useHttp) {
196
+ // In HTTP mode, we can prompt for the API key
197
+ logger.error("SOCKET_API_KEY environment variable is not set");
198
+ SOCKET_API_KEY = await getApiKeyInteractively();
199
+ }
200
+ else {
201
+ // In stdio mode, we must have the API key as an environment variable
202
+ logger.error("SOCKET_API_KEY environment variable is required in stdio mode");
203
+ logger.error("Please set the SOCKET_API_KEY environment variable and try again");
204
+ process.exit(1);
205
+ }
206
+ }
207
+ // Now that we have the API key, set up the headers
208
+ const SOCKET_HEADERS = {
209
+ "user-agent": `socket-mcp/${VERSION}`,
210
+ "accept": "application/x-ndjson",
211
+ "content-type": "application/json",
212
+ "authorization": `Bearer ${SOCKET_API_KEY}`
213
+ };
214
+ if (useHttp) {
215
+ // HTTP mode with Server-Sent Events
216
+ logger.info(`Starting HTTP server on port ${port}`);
217
+ const httpServer = createServer(async (req, res) => {
218
+ // Enable CORS
219
+ res.setHeader('Access-Control-Allow-Origin', '*');
220
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
221
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
222
+ if (req.method === 'OPTIONS') {
223
+ res.writeHead(200);
224
+ res.end();
225
+ return;
226
+ }
227
+ const url = new URL(req.url, `http://localhost:${port}`);
228
+ if (url.pathname === '/mcp') {
229
+ if (req.method === 'POST') {
230
+ // Handle JSON-RPC messages
231
+ let body = '';
232
+ req.on('data', chunk => body += chunk);
233
+ req.on('end', async () => {
234
+ try {
235
+ const jsonData = JSON.parse(body);
236
+ const sessionId = req.headers['mcp-session-id'];
237
+ let transport;
238
+ if (sessionId && transports[sessionId]) {
239
+ // Reuse existing transport
240
+ transport = transports[sessionId];
241
+ }
242
+ else if (!sessionId && isInitializeRequest(jsonData)) {
243
+ // New initialization request
244
+ transport = new StreamableHTTPServerTransport({
245
+ sessionIdGenerator: () => randomUUID(),
246
+ onsessioninitialized: (id) => {
247
+ transports[id] = transport;
248
+ logger.info(`Session initialized: ${id}`);
249
+ }
250
+ });
251
+ transport.onclose = () => {
252
+ const sid = transport.sessionId;
253
+ if (sid && transports[sid]) {
254
+ delete transports[sid];
255
+ logger.info(`Session closed: ${sid}`);
256
+ }
257
+ };
258
+ await server.connect(transport);
259
+ await transport.handleRequest(req, res, jsonData);
260
+ return;
261
+ }
262
+ else {
263
+ // Invalid request
264
+ res.writeHead(400);
265
+ res.end(JSON.stringify({
266
+ jsonrpc: '2.0',
267
+ error: { code: -32000, message: 'Bad Request: No valid session ID' },
268
+ id: null
269
+ }));
270
+ return;
271
+ }
272
+ // Handle request with existing transport
273
+ await transport.handleRequest(req, res, jsonData);
274
+ }
275
+ catch (error) {
276
+ logger.error(`Error processing POST request: ${error}`);
277
+ if (!res.headersSent) {
278
+ res.writeHead(500);
279
+ res.end(JSON.stringify({
280
+ jsonrpc: '2.0',
281
+ error: { code: -32603, message: 'Internal server error' },
282
+ id: null
283
+ }));
284
+ }
285
+ }
286
+ });
287
+ }
288
+ else if (req.method === 'GET') {
289
+ // Handle SSE streams
290
+ const sessionId = req.headers['mcp-session-id'];
291
+ if (!sessionId || !transports[sessionId]) {
292
+ res.writeHead(400);
293
+ res.end('Invalid or missing session ID');
294
+ return;
295
+ }
296
+ const transport = transports[sessionId];
297
+ await transport.handleRequest(req, res);
298
+ }
299
+ else if (req.method === 'DELETE') {
300
+ // Handle session termination
301
+ const sessionId = req.headers['mcp-session-id'];
302
+ if (!sessionId || !transports[sessionId]) {
303
+ res.writeHead(400);
304
+ res.end('Invalid or missing session ID');
305
+ return;
306
+ }
307
+ const transport = transports[sessionId];
308
+ await transport.handleRequest(req, res);
309
+ }
310
+ else {
311
+ res.writeHead(405);
312
+ res.end('Method not allowed');
313
+ }
314
+ }
315
+ else {
316
+ res.writeHead(404);
317
+ res.end('Not found');
318
+ }
319
+ });
320
+ httpServer.listen(port, () => {
321
+ logger.info(`Socket MCP HTTP server version ${VERSION} started successfully on port ${port}`);
322
+ logger.info(`Connect to: http://localhost:${port}/mcp`);
323
+ });
324
+ }
325
+ else {
326
+ // Stdio mode (default)
327
+ logger.info("Starting in stdio mode");
328
+ const transport = new StdioServerTransport();
329
+ server.connect(transport)
330
+ .then(() => {
331
+ logger.info(`Socket MCP server version ${VERSION} started successfully`);
332
+ })
333
+ .catch((error) => {
334
+ logger.error(`Failed to start Socket MCP server: ${error.message}`);
335
+ process.exit(1);
336
+ });
337
+ }
338
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "@socketsecurity/mcp",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "./build/index.js",
6
6
  "bin": {
7
7
  "socket-mcp": "./build/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "tsc && chmod 755 ./build/index.js",
11
- "postinstall": "npm run build"
10
+ "build": "tsc && chmod 755 ./build/index.js && (chmod 755 ./build/mock-client/*.js 2>/dev/null || true)",
11
+ "debug:stdio": "node ./build/mock-client/debug-client.js",
12
+ "debug:sdk": "node ./build/mock-client/stdio-client.js",
13
+ "debug:http": "node ./build/mock-client/http-client.js",
14
+ "server:stdio": "SOCKET_API_KEY=${SOCKET_API_KEY} ./build/index.js",
15
+ "server:http": "MCP_HTTP_MODE=true SOCKET_API_KEY=${SOCKET_API_KEY} ./build/index.js"
12
16
  },
13
17
  "files": [
14
18
  "build"
@@ -16,12 +20,17 @@
16
20
  "keywords": [],
17
21
  "author": "Alexandros Kapravelos",
18
22
  "description": "Socket MCP server for scanning dependencies",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/SocketDev/socket-mcp"
26
+ },
19
27
  "dependencies": {
20
28
  "@modelcontextprotocol/sdk": "^1.11.3",
21
29
  "@types/node-fetch": "^2.6.12",
22
30
  "node-fetch": "^3.3.2",
31
+ "pino": "^9.7.0",
32
+ "pino-pretty": "^13.0.0",
23
33
  "semver": "^7.7.2",
24
- "winston": "^3.17.0",
25
34
  "zod": "^3.24.4"
26
35
  },
27
36
  "devDependencies": {