dist-mcp / mcp / config.js
dist-mcp / mcp / config.js
import { optionalEnv, resolveMadeForEnv } from "./core-bundle.mjs";
import { getLogsDir, getPluginLogFilename } from "../core-bundle.mjs";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Matches toolsProvider.ts's localTimestamp() exactly, so entries from both
// entrypoints read consistently in the shared plugin log file.
function localTimestamp() {
try {
return new Date().toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
}
catch {
return new Date().toString();
}
}
/**
* CHAT_WORKING_DIRECTORIES has no single universal default — each client points its scratchpads
* somewhere different (bionic: LM Studio's own scratchpads root; unsloth: its sandbox dir under
* studio's own data folder; generic: the user's own Pictures folder, since a generic/third-party
* client isn't necessarily writing into an LM Studio-owned scratchpad).
*/
function defaultChatWorkingDirectories(preset) {
if (preset.name === "unsloth")
return path.join(os.homedir(), ".unsloth", "studio", "sandbox");
if (preset.name === "generic")
return path.join(os.homedir(), "Pictures");
return path.join(os.homedir(), ".lmstudio", "scratchpads");
}
/** Keeps each preset's local HTTP server on its own port so bionic/unsloth/generic MCP servers can run side by side without colliding. Same scheme as every other bridge — all must agree on the same port per preset. */
function defaultHttpServerPort(preset) {
if (preset.name === "unsloth")
return 54780;
if (preset.name === "generic")
return 54790;
return 54770;
}
function parseBoolean(value, fallback) {
if (value === undefined)
return fallback;
return /^(1|true|yes)$/i.test(value.trim());
}
function parsePort(name, value, fallback) {
if (value === undefined)
return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive integer, got "${value}".`);
}
return parsed;
}
/** Every env var readMcpConfig() reads — used to log a complete, unfiltered snapshot. */
const MCP_ENV_VAR_NAMES = [
"CHAT_WORKING_DIRECTORIES",
"MCP_MADE_FOR_BIONIC", // primary, documented selector (see README_MCP.md)
"MCP_MADE_FOR", // documented alternate selector for the unsloth/generic presets (see README_MCP.md)
"DRAW_THINGS_HOST",
"DRAW_THINGS_HTTP_PORT",
"DRAW_THINGS_GRPC_PORT",
"EMBED_PNG_METADATA",
"DRAW_THINGS_CUSTOM_CONFIGS_PATH",
"HTTP_SERVER_PORT",
"CLIENT_DB_LOCATION", // only read by the unsloth preset's beforeToolCall hook
];
/**
* Raw process.env values for every var readMcpConfig() reads, exactly as received —
* unset stays `null` (never silently coalesced to a default), set-but-empty stays `""`.
* Callers should log this BEFORE readMcpConfig() so a required-env failure still leaves
* a full record of what was actually passed in.
*/
export function getRawEnvSnapshot() {
const snapshot = {};
for (const name of MCP_ENV_VAR_NAMES) {
snapshot[name] = process.env[name] ?? null;
}
return snapshot;
}
export function readMcpConfig() {
const preset = resolveMadeForEnv(optionalEnv("MCP_MADE_FOR"), optionalEnv("MCP_MADE_FOR_BIONIC"));
return {
// Not a literal "~" string: nothing downstream expands tildes for this path (unlike
// customConfigsPath, which customConfigsLoader.ts explicitly expands), so the default is
// computed here via os.homedir() instead.
chatWorkingDirectories: optionalEnv("CHAT_WORKING_DIRECTORIES") ?? defaultChatWorkingDirectories(preset),
preset,
previewInChat: preset.includeBase64Preview,
drawThingsHost: optionalEnv("DRAW_THINGS_HOST") ?? "127.0.0.1",
drawThingsHttpPort: parsePort("DRAW_THINGS_HTTP_PORT", optionalEnv("DRAW_THINGS_HTTP_PORT"), 7860),
drawThingsGrpcPort: parsePort("DRAW_THINGS_GRPC_PORT", optionalEnv("DRAW_THINGS_GRPC_PORT"), 7859),
embedPngMetadata: parseBoolean(optionalEnv("EMBED_PNG_METADATA"), true),
customConfigsPath: optionalEnv("DRAW_THINGS_CUSTOM_CONFIGS_PATH") ?? "~/Library/Containers/com.liuliu.draw-things/Data/Documents/Models/custom_configs.json",
// Plain HTTP_SERVER_PORT, no repo prefix — every bridge/preset must agree on the same port name and the same preset-based default.
httpServerPort: parsePort("HTTP_SERVER_PORT", optionalEnv("HTTP_SERVER_PORT"), defaultHttpServerPort(preset)),
clientDbLocation: optionalEnv("CLIENT_DB_LOCATION") ?? path.join(os.homedir(), ".unsloth", "studio", "studio.db"),
};
}
/** Sets the process.env values core/tools.ts reads directly (see getCurrentConnectionSettings, EMBED_PNG_METADATA check). */
export function applyMcpConfig(config) {
process.env.PREVIEW_IN_CHAT = config.previewInChat ? "true" : "false";
process.env.DRAW_THINGS_HOST = config.drawThingsHost;
process.env.DRAW_THINGS_HTTP_PORT = String(config.drawThingsHttpPort);
process.env.DRAW_THINGS_GRPC_PORT = String(config.drawThingsGrpcPort);
process.env.EMBED_PNG_METADATA = config.embedPngMetadata ? "true" : "false";
process.env.HTTP_SERVER_PORT = String(config.httpServerPort);
}
function logPreflight(line) {
try {
const logsDir = getLogsDir();
if (!fs.existsSync(logsDir))
fs.mkdirSync(logsDir, { recursive: true });
fs.appendFileSync(path.join(logsDir, getPluginLogFilename()), `${localTimestamp()} - ${line}\n`);
}
catch {
// Never let logging itself break startup.
}
}
export async function initCustomConfigs(customConfigsPath) {
const trimmed = customConfigsPath.trim();
if (!trimmed || trimmed.toLowerCase() === "none") {
logPreflight("Custom Configs: disabled");
return { status: "disabled" };
}
const { checkCustomConfigs, loadCustomConfigs } = await import("../services/customConfigsLoader.js");
const status = await checkCustomConfigs(trimmed);
if (status.available) {
logPreflight(`Custom Configs: found at ${status.filePath}`);
const presets = await loadCustomConfigs();
logPreflight(`Custom Configs: loaded ${presets.size} presets`);
return { status: "loaded", filePath: status.filePath, presetCount: presets.size };
}
const error = status.error || "custom_configs.json not found";
logPreflight(`Custom Configs: not available (${error})`);
return { status: "not-available", error };
}
import { optionalEnv, resolveMadeForEnv } from "./core-bundle.mjs";
import { getLogsDir, getPluginLogFilename } from "../core-bundle.mjs";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Matches toolsProvider.ts's localTimestamp() exactly, so entries from both
// entrypoints read consistently in the shared plugin log file.
function localTimestamp() {
try {
return new Date().toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
}
catch {
return new Date().toString();
}
}
/**
* CHAT_WORKING_DIRECTORIES has no single universal default — each client points its scratchpads
* somewhere different (bionic: LM Studio's own scratchpads root; unsloth: its sandbox dir under
* studio's own data folder; generic: the user's own Pictures folder, since a generic/third-party
* client isn't necessarily writing into an LM Studio-owned scratchpad).
*/
function defaultChatWorkingDirectories(preset) {
if (preset.name === "unsloth")
return path.join(os.homedir(), ".unsloth", "studio", "sandbox");
if (preset.name === "generic")
return path.join(os.homedir(), "Pictures");
return path.join(os.homedir(), ".lmstudio", "scratchpads");
}
/** Keeps each preset's local HTTP server on its own port so bionic/unsloth/generic MCP servers can run side by side without colliding. Same scheme as every other bridge — all must agree on the same port per preset. */
function defaultHttpServerPort(preset) {
if (preset.name === "unsloth")
return 54780;
if (preset.name === "generic")
return 54790;
return 54770;
}
function parseBoolean(value, fallback) {
if (value === undefined)
return fallback;
return /^(1|true|yes)$/i.test(value.trim());
}
function parsePort(name, value, fallback) {
if (value === undefined)
return fallback;
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive integer, got "${value}".`);
}
return parsed;
}
/** Every env var readMcpConfig() reads — used to log a complete, unfiltered snapshot. */
const MCP_ENV_VAR_NAMES = [
"CHAT_WORKING_DIRECTORIES",
"MCP_MADE_FOR_BIONIC", // primary, documented selector (see README_MCP.md)
"MCP_MADE_FOR", // documented alternate selector for the unsloth/generic presets (see README_MCP.md)
"DRAW_THINGS_HOST",
"DRAW_THINGS_HTTP_PORT",
"DRAW_THINGS_GRPC_PORT",
"EMBED_PNG_METADATA",
"DRAW_THINGS_CUSTOM_CONFIGS_PATH",
"HTTP_SERVER_PORT",
"CLIENT_DB_LOCATION", // only read by the unsloth preset's beforeToolCall hook
];
/**
* Raw process.env values for every var readMcpConfig() reads, exactly as received —
* unset stays `null` (never silently coalesced to a default), set-but-empty stays `""`.
* Callers should log this BEFORE readMcpConfig() so a required-env failure still leaves
* a full record of what was actually passed in.
*/
export function getRawEnvSnapshot() {
const snapshot = {};
for (const name of MCP_ENV_VAR_NAMES) {
snapshot[name] = process.env[name] ?? null;
}
return snapshot;
}
export function readMcpConfig() {
const preset = resolveMadeForEnv(optionalEnv("MCP_MADE_FOR"), optionalEnv("MCP_MADE_FOR_BIONIC"));
return {
// Not a literal "~" string: nothing downstream expands tildes for this path (unlike
// customConfigsPath, which customConfigsLoader.ts explicitly expands), so the default is
// computed here via os.homedir() instead.
chatWorkingDirectories: optionalEnv("CHAT_WORKING_DIRECTORIES") ?? defaultChatWorkingDirectories(preset),
preset,
previewInChat: preset.includeBase64Preview,
drawThingsHost: optionalEnv("DRAW_THINGS_HOST") ?? "127.0.0.1",
drawThingsHttpPort: parsePort("DRAW_THINGS_HTTP_PORT", optionalEnv("DRAW_THINGS_HTTP_PORT"), 7860),
drawThingsGrpcPort: parsePort("DRAW_THINGS_GRPC_PORT", optionalEnv("DRAW_THINGS_GRPC_PORT"), 7859),
embedPngMetadata: parseBoolean(optionalEnv("EMBED_PNG_METADATA"), true),
customConfigsPath: optionalEnv("DRAW_THINGS_CUSTOM_CONFIGS_PATH") ?? "~/Library/Containers/com.liuliu.draw-things/Data/Documents/Models/custom_configs.json",
// Plain HTTP_SERVER_PORT, no repo prefix — every bridge/preset must agree on the same port name and the same preset-based default.
httpServerPort: parsePort("HTTP_SERVER_PORT", optionalEnv("HTTP_SERVER_PORT"), defaultHttpServerPort(preset)),
clientDbLocation: optionalEnv("CLIENT_DB_LOCATION") ?? path.join(os.homedir(), ".unsloth", "studio", "studio.db"),
};
}
/** Sets the process.env values core/tools.ts reads directly (see getCurrentConnectionSettings, EMBED_PNG_METADATA check). */
export function applyMcpConfig(config) {
process.env.PREVIEW_IN_CHAT = config.previewInChat ? "true" : "false";
process.env.DRAW_THINGS_HOST = config.drawThingsHost;
process.env.DRAW_THINGS_HTTP_PORT = String(config.drawThingsHttpPort);
process.env.DRAW_THINGS_GRPC_PORT = String(config.drawThingsGrpcPort);
process.env.EMBED_PNG_METADATA = config.embedPngMetadata ? "true" : "false";
process.env.HTTP_SERVER_PORT = String(config.httpServerPort);
}
function logPreflight(line) {
try {
const logsDir = getLogsDir();
if (!fs.existsSync(logsDir))
fs.mkdirSync(logsDir, { recursive: true });
fs.appendFileSync(path.join(logsDir, getPluginLogFilename()), `${localTimestamp()} - ${line}\n`);
}
catch {
// Never let logging itself break startup.
}
}
export async function initCustomConfigs(customConfigsPath) {
const trimmed = customConfigsPath.trim();
if (!trimmed || trimmed.toLowerCase() === "none") {
logPreflight("Custom Configs: disabled");
return { status: "disabled" };
}
const { checkCustomConfigs, loadCustomConfigs } = await import("../services/customConfigsLoader.js");
const status = await checkCustomConfigs(trimmed);
if (status.available) {
logPreflight(`Custom Configs: found at ${status.filePath}`);
const presets = await loadCustomConfigs();
logPreflight(`Custom Configs: loaded ${presets.size} presets`);
return { status: "loaded", filePath: status.filePath, presetCount: presets.size };
}
const error = status.error || "custom_configs.json not found";
logPreflight(`Custom Configs: not available (${error})`);
return { status: "not-available", error };
}