diff --git a/lib/depscore-tool.ts b/lib/depscore-tool.ts index e8d728e..0ba8cdd 100644 --- a/lib/depscore-tool.ts +++ b/lib/depscore-tool.ts @@ -1,4 +1,5 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { envAsBoolean } from '@socketsecurity/lib/env/boolean' import { getSocketDebug } from '@socketsecurity/lib/env/socket' import { getSocketApiToken, getSocketApiUrl } from './env.ts' import { httpRequest } from '@socketsecurity/lib/http-request/request' @@ -12,6 +13,7 @@ import { registerAlertsTool } from './alerts-tool.ts' import { registerOrganizationsTool } from './organizations-tool.ts' import { registerPackageFilesTools } from './package-files-tool.ts' import { registerThreatFeedTool } from './threat-feed-tool.ts' +import { withToolLogging } from './tool-logging.ts' interface DepscorePackageInput { ecosystem?: string | undefined @@ -34,7 +36,7 @@ interface ToolOkResult { // stack development; the default targets production. Both env vars // resolved via fleet-canonical helpers. const DEFAULT_SOCKET_API_URL = - getSocketDebug() === 'true' + envAsBoolean(getSocketDebug()) ? 'http://localhost:8866/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false' : 'https://api.socket.dev/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false' @@ -97,7 +99,9 @@ export function buildPackageComponents( // Build a configured McpServer with the depscore tool registered. // Used for stdio (single instance) and HTTP (one per session). export function createConfiguredServer(): McpServer { - const srv = new McpServer({ name: 'socket', version: VERSION }) + const srv = withToolLogging( + new McpServer({ name: 'socket', version: VERSION }), + ) srv.registerTool( 'depscore', { @@ -153,6 +157,12 @@ export function formatScoreLine(jsonData: Record): string { return `${purl}: No score found` } +// Read the boot-time static API key. Used by tool modules outside this file +// that share the same token-resolution chain as depscore. +export function getStaticApiKey(): string { + return staticApiKey +} + // Build the depscore handler — pulled out so the MCP registration is // readable. The handler closes over the access token retrieval chain // (request authInfo → env token). @@ -276,12 +286,6 @@ export function parseSinglePackageBody(responseText: string): string[] { return [formatScoreLine(jsonData)] } -// Read the boot-time static API key. Used by tool modules outside this file -// that share the same token-resolution chain as depscore. -export function getStaticApiKey(): string { - return staticApiKey -} - // Set the static API key. Called once during boot from index.ts. // Subsequent calls overwrite — only the most recent value is used. export function setStaticApiKey(value: string): void { diff --git a/lib/logger.ts b/lib/logger.ts index a8b6763..01136b8 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -2,12 +2,23 @@ import os from 'node:os' import path from 'node:path' import pino from 'pino' -// Pino logger writing info-level to socket-mcp.log and errors to +import { envBool, envString } from './env.ts' + +// Debug mode raises verbosity to `debug` and streams pretty logs to stderr +// so tool calls and error responses are visible live in the terminal. +// Enable with MCP_DEBUG=true (the `server-*:debug` scripts set it) or by +// setting LOG_LEVEL explicitly. stderr is safe in both stdio and HTTP modes +// — it is never the MCP protocol channel (stdout is). +const debug = envBool('MCP_DEBUG') || envString('LOG_LEVEL') === 'debug' +const level = envString('LOG_LEVEL') ?? (debug ? 'debug' : 'info') + +// Pino logger writing the chosen level to socket-mcp.log and errors to // socket-mcp-error.log under the platform tmp directory. Two file targets // instead of one give grep-friendly error isolation without losing the -// info stream. +// info stream. A pretty stderr target surfaces errors to the terminal +// always, and the full debug stream when debug mode is on. export const logger = pino({ - level: 'info', + level, transport: { targets: [ { @@ -22,6 +33,11 @@ export const logger = pino({ options: { destination: path.join(os.tmpdir(), 'socket-mcp.log') }, level: 'info', }, + { + target: 'pino-pretty', + options: { destination: 2 }, + level: debug ? 'debug' : 'error', + }, ], }, }) diff --git a/lib/tool-logging.ts b/lib/tool-logging.ts new file mode 100644 index 0000000..ce9874f --- /dev/null +++ b/lib/tool-logging.ts @@ -0,0 +1,56 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' + +import { logger } from './logger.ts' + +interface ToolResult { + content?: unknown | undefined + isError?: boolean | undefined +} + +// Wrap srv.registerTool so every tool logs its invocation args and its +// response. Request args + successful responses log at `debug` (enable with +// MCP_DEBUG=true); error responses and thrown errors always log at `error` +// so failures surface even in a normal run. Args carry no secrets — the +// access token rides on `extra.authInfo`, which is never logged here. +// +// Applied centrally in createConfiguredServer so all tools get the same +// treatment without each handler repeating the logging. +export function withToolLogging(srv: McpServer): McpServer { + const original = srv.registerTool.bind(srv) as McpServer['registerTool'] + srv.registerTool = (( + name: string, + config: unknown, + handler: (...handlerArgs: unknown[]) => unknown, + ) => { + const wrapped = async (...callArgs: unknown[]): Promise => { + // 2-arg handlers (no input schema) get only `extra`; 3-arg form here + // means the first call arg is the tool's parsed input. + const args = callArgs.length > 1 ? callArgs[0] : undefined + logger.debug({ tool: name, args }, 'tool call') + try { + const result = (await handler(...callArgs)) as ToolResult + if (result?.isError) { + logger.error( + { tool: name, response: result.content }, + 'tool call returned error', + ) + } else { + logger.debug({ tool: name, response: result?.content }, 'tool result') + } + return result + } catch (e) { + logger.error( + { tool: name, error: e instanceof Error ? e.message : String(e) }, + 'tool call threw', + ) + throw e + } + } + return original( + name, + config as Parameters[1], + wrapped as Parameters[2], + ) + }) as McpServer['registerTool'] + return srv +} diff --git a/package.json b/package.json index a8affe4..35db1e1 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,28 @@ { "name": "@socketsecurity/mcp", "version": "0.0.17", - "type": "module", - "main": "./index.js", + "description": "Socket MCP server for scanning dependencies", + "keywords": [], + "author": "Alexandros Kapravelos", + "repository": { + "type": "git", + "url": "https://github.com/SocketDev/socket-mcp" + }, "bin": { "socket-mcp": "./index.js" }, - "engines": { - "node": ">=22.0.0", - "npm": ">=11.15.0" - }, - "packageManager": "pnpm@11.3.0", + "files": [ + "package.json", + "index.js", + "index.d.ts", + "index.d.ts.map", + "lib/**/*.js", + "lib/**/*.d.ts", + "mock-client/**/*.js", + "mock-client/**/*.d.ts*" + ], + "type": "module", + "main": "./index.js", "scripts": { "prepare": "node scripts/install-git-hooks.mts", "prepublishOnly": "npm run build", @@ -40,30 +52,15 @@ "debug-sdk": "node --experimental-strip-types ./mock-client/stdio-client.ts", "debug-http": "node --experimental-strip-types ./mock-client/http-client.ts", "server-stdio": "SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node --experimental-strip-types ./index.ts", + "server-stdio:debug": "MCP_DEBUG=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node --experimental-strip-types ./index.ts", "server-http": "MCP_HTTP_MODE=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node --experimental-strip-types ./index.ts", + "server-http:debug": "MCP_DEBUG=true MCP_HTTP_MODE=true SOCKET_API_TOKEN=${SOCKET_API_TOKEN:-${SOCKET_API_KEY}} node --experimental-strip-types ./index.ts", "check:paths": "node scripts/check-paths.mts", "security": "node scripts/security.mts", "setup-security-tools": "node .claude/hooks/setup-security-tools/install.mts", "lockstep": "node scripts/lockstep.mts", "lockstep:emit-schema": "node scripts/lockstep-emit-schema.mts" }, - "keywords": [], - "files": [ - "package.json", - "index.js", - "index.d.ts", - "index.d.ts.map", - "lib/**/*.js", - "lib/**/*.d.ts", - "mock-client/**/*.js", - "mock-client/**/*.d.ts*" - ], - "author": "Alexandros Kapravelos", - "description": "Socket MCP server for scanning dependencies", - "repository": { - "type": "git", - "url": "https://github.com/SocketDev/socket-mcp" - }, "dependencies": { "@anthropic-ai/mcpb": "^1.2.0", "@modelcontextprotocol/sdk": "1.26.0", @@ -75,6 +72,18 @@ "semver": "^7.7.4", "zod": "3.25.76" }, + "devDependencies": { + "@sinclair/typebox": "catalog:", + "@types/node": "^24.12.3", + "@types/semver": "^7.7.1", + "@types/triple-beam": "^1.3.5", + "@typescript/native-preview": "7.0.0-dev.20260510.1", + "npm-run-all2": "^8.0.4", + "oxfmt": "0.48.0", + "oxlint": "1.63.0", + "taze": "catalog:", + "typescript": "~5.9.3" + }, "overrides": { "@hono/node-server": "1.19.13", "fast-uri": "3.1.2", @@ -86,6 +95,11 @@ "zod": "3.25.76", "zod-to-json-schema": "3.25.1" }, + "engines": { + "node": ">=22.0.0", + "npm": ">=11.15.0" + }, + "packageManager": "pnpm@11.3.0", "pnpm": { "overrides": { "@hono/node-server": "1.19.13", @@ -99,18 +113,6 @@ "zod-to-json-schema": "3.25.1" } }, - "devDependencies": { - "@sinclair/typebox": "catalog:", - "@types/node": "^24.12.3", - "@types/semver": "^7.7.1", - "@types/triple-beam": "^1.3.5", - "@typescript/native-preview": "7.0.0-dev.20260510.1", - "npm-run-all2": "^8.0.4", - "oxfmt": "0.48.0", - "oxlint": "1.63.0", - "taze": "catalog:", - "typescript": "~5.9.3" - }, "npm-run-all2": { "nodeRun": true }