/**
* Settings Manager
*
* Handles persistent configuration for the NotebookLM MCP Server.
* Manages profiles, disabled tools, and environment variable overrides.
*/
import fs from "fs/promises";
import { existsSync, mkdirSync } from "fs";
import path from "path";
import { CONFIG } from "../config.js";
import { log } from "./logger.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
export type ProfileName = "minimal" | "standard" | "full";
export interface Settings {
profile: ProfileName;
disabledTools: string[];
customSettings?: Record<string, any>;
}
const DEFAULT_SETTINGS: Settings = {
profile: "full",
disabledTools: [],
};
const PROFILES: Record<ProfileName, string[]> = {
minimal: [
"ask_question",
"get_health",
"list_notebooks",
"select_notebook",
"get_notebook" // Added as it is read-only and useful
],
standard: [
"ask_question",
"get_health",
"list_notebooks",
"select_notebook",
"get_notebook",
"setup_auth",
"list_sessions",
"add_notebook",
"update_notebook",
"search_notebooks"
],
full: ["*"] // All tools
};
export class SettingsManager {
private settingsPath: string;
private settings: Settings;
constructor() {
// Use the config directory from env-paths defined in config.ts
this.settingsPath = path.join(CONFIG.configDir, "settings.json");
this.settings = this.loadSettings();
}
/**
* Load settings from file, falling back to defaults
*/
private loadSettings(): Settings {
try {
// Ensure config dir exists
if (!existsSync(CONFIG.configDir)) {
mkdirSync(CONFIG.configDir, { recursive: true });
}
if (existsSync(this.settingsPath)) {
// Use fs.readFileSync for synchronous initialization in constructor if needed,
// but here we used async fs in imports. For simplicity in constructor,
// we'll assume the file is read when needed or require explicit init.
// Actually, to keep it simple, let's use require/import or readFileSync.
const fsSync = require("fs");
const data = fsSync.readFileSync(this.settingsPath, "utf-8");
return { ...DEFAULT_SETTINGS, ...JSON.parse(data) };
}
} catch (error) {
log.warning(`⚠️ Failed to load settings: ${error}. Using defaults.`);
}
return { ...DEFAULT_SETTINGS };
}
/**
* Save current settings to file
*/
async saveSettings(newSettings: Partial<Settings>): Promise<void> {
this.settings = { ...this.settings, ...newSettings };
try {
await fs.writeFile(this.settingsPath, JSON.stringify(this.settings, null, 2), "utf-8");
} catch (error) {
throw new Error(`Failed to save settings: ${error}`);
}
}
/**
* Get effective configuration (merging File settings with Env Vars)
*/
getEffectiveSettings(): Settings {
const envProfile = process.env.NOTEBOOKLM_PROFILE as ProfileName;
const envDisabled = process.env.NOTEBOOKLM_DISABLED_TOOLS;
const effectiveProfile = (envProfile && PROFILES[envProfile]) ? envProfile : this.settings.profile;
let effectiveDisabled = [...this.settings.disabledTools];
if (envDisabled) {
const envDisabledList = envDisabled.split(",").map(t => t.trim());
effectiveDisabled = [...new Set([...effectiveDisabled, ...envDisabledList])];
}
return {
profile: effectiveProfile,
disabledTools: effectiveDisabled,
customSettings: this.settings.customSettings
};
}
/**
* Filter tools based on effective configuration
*/
filterTools(allTools: Tool[]): Tool[] {
const { profile, disabledTools } = this.getEffectiveSettings();
const allowedTools = PROFILES[profile];
return allTools.filter(tool => {
// 1. Check if allowed by profile (unless profile is full/wildcard)
if (!allowedTools.includes("*") && !allowedTools.includes(tool.name)) {
return false;
}
// 2. Check if explicitly disabled
if (disabledTools.includes(tool.name)) {
return false;
}
return true;
});
}
getSettingsPath(): string {
return this.settingsPath;
}
getProfiles(): Record<ProfileName, string[]> {
return PROFILES;
}
}