socraticode
Version:
SocratiCode — MCP server for local codebase indexing, semantic search, and code dependency graphs. All private, all local via Docker.
123 lines • 4.16 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
/**
* Structured logger for SocratiCode MCP server.
* Outputs JSON-structured log lines to stderr (stdout is reserved for MCP JSON-RPC).
* Also forwards log entries as MCP notifications/message so hosts like Cline
* can display them in their UI.
*
* When SOCRATICODE_LOG_FILE is set to an absolute path, all log entries are
* also appended to that file — useful for debugging when the MCP host doesn't
* surface log notifications.
*/
import { appendFileSync } from "node:fs";
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const envLevel = process.env.SOCRATICODE_LOG_LEVEL?.toLowerCase();
const initialLevel = envLevel && Object.hasOwn(LOG_LEVELS, envLevel)
? envLevel
: "info";
let currentLevel = initialLevel;
const logFilePath = process.env.SOCRATICODE_LOG_FILE || undefined;
// Write a separator so you can tell where each server session begins
if (logFilePath) {
try {
appendFileSync(logFilePath, `\n--- SocratiCode session started at ${new Date().toISOString()} ---\n`);
}
catch {
// Can't write — will be caught again when we try to append.
}
}
function shouldLog(level) {
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
}
/**
* Override the active log level. Intended for tests so they don't depend on
* `SOCRATICODE_LOG_LEVEL` from the host shell. Production callers normally
* leave this alone and rely on the env-derived initial level.
*/
export function setLogLevel(level) {
currentLevel = level;
}
/** Get the active log level. Useful for tests asserting their setup. */
export function getLogLevel() {
return currentLevel;
}
function formatLog(level, message, context) {
const entry = {
timestamp: new Date().toISOString(),
level,
data: message,
...context,
};
return JSON.stringify(entry);
}
/** Maps our log levels to the MCP spec level names. */
function toMcpLevel(level) {
return level === "warn" ? "warning" : level;
}
let _mcpLogSender = null;
/**
* Register a function that forwards log entries as MCP notifications/message.
* Called once from index.ts immediately after `new McpServer(...)`.
*/
export function setMcpLogSender(fn) {
_mcpLogSender = fn;
}
function sendMcpLog(level, message) {
if (_mcpLogSender) {
try {
_mcpLogSender({ level: toMcpLevel(level), logger: "socraticode", data: message });
}
catch {
// Transport not yet connected or already closed — silently ignore.
}
}
}
function emit(level, message, context) {
if (!shouldLog(level))
return;
// Always write to log file when configured — this is the reliable path
if (logFilePath) {
try {
appendFileSync(logFilePath, `${formatLog(level, message, context)}\n`);
}
catch {
// Silently ignore write errors
}
}
if (_mcpLogSender) {
// MCP transport is active: route via notifications/message only.
// Writing to stderr while hosted inside Cline causes Cline to emit
// "LOG Server "name" info:" with the content silently dropped (Cline's
// Logger only appends extra args in IS_DEV mode).
// Include context in the message string so error details aren't silently dropped.
const fullMessage = context && Object.keys(context).length > 0
? `${message} ${JSON.stringify(context)}`
: message;
sendMcpLog(level, fullMessage);
}
else {
// No MCP transport yet (startup, tests, direct invocation): write JSON to stderr.
process.stderr.write(`${formatLog(level, message, context)}\n`);
}
}
export const logger = {
debug(message, context) {
emit("debug", message, context);
},
info(message, context) {
emit("info", message, context);
},
warn(message, context) {
emit("warn", message, context);
},
error(message, context) {
emit("error", message, context);
},
};
//# sourceMappingURL=logger.js.map