/**
* Settings Manager
*
* Handles persistent configuration for the NotebookLM MCP Server.
* Manages profiles, disabled tools, and environment variable overrides.
*/
import { existsSync, readFileSync } from "fs";
import path from "path";
import { CONFIG } from "../config.js";
import { log } from "./logger.js";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { mkdirSecure, writeFileSecure, PERMISSION_MODES } from "./file-permissions.js";
export type ProfileName = "minimal" | "standard" | "full";
export interface Settings {
profile: ProfileName;
disabledTools: string[];
customSettings?: Record<string, any>;
}
const DEFAULT_SETTINGS: Settings = {
profile: "standard",
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: [
// Query & research
"ask_question",
"get_notebook_chat_history",
"get_query_history",
// Library management
"list_notebooks",
"select_notebook",
"get_notebook",
"add_notebook",
"update_notebook",
"remove_notebook",
"search_notebooks",
"sync_library",
// Notebook creation & source management
"create_notebook",
"batch_create_notebooks",
"list_sources",
"add_source",
"remove_source",
// Studio features (audio, video, data tables)
"generate_audio_overview",
"get_audio_status",
"download_audio",
"generate_video_overview",
"get_video_status",
"generate_data_table",
"get_data_table",
// Session & system
"get_health",
"setup_auth",
"re_auth",
"list_sessions",
"close_session",
"reset_session",
"get_quota",
"cleanup_data"
],
full: ["*"] // All tools
};
/**
* Gemini API tools that can be disabled via NOTEBOOKLM_NO_GEMINI=true
* These tools require GEMINI_API_KEY and may not be needed by all clients
*/
const GEMINI_TOOLS = [
"deep_research",
"gemini_query",
"get_research_status",
"upload_document",
"query_document",
"list_documents",
"delete_document",
"query_chunked_document",
];
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 with secure permissions
if (!existsSync(CONFIG.configDir)) {
mkdirSecure(CONFIG.configDir, PERMISSION_MODES.OWNER_FULL);
}
if (existsSync(this.settingsPath)) {
const data = readFileSync(this.settingsPath, "utf-8");
const parsed = JSON.parse(data);
// Validate parsed settings to prevent property injection
const validated: Partial<Settings> = {};
if (parsed.profile && ["minimal", "standard", "full"].includes(parsed.profile)) {
validated.profile = parsed.profile;
}
if (Array.isArray(parsed.disabledTools)) {
validated.disabledTools = parsed.disabledTools.filter((t: unknown) => typeof t === "string");
}
if (parsed.customSettings && typeof parsed.customSettings === "object" && !Array.isArray(parsed.customSettings)) {
validated.customSettings = parsed.customSettings;
}
return { ...DEFAULT_SETTINGS, ...validated };
}
} 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 {
writeFileSecure(
this.settingsPath,
JSON.stringify(this.settings, null, 2),
PERMISSION_MODES.OWNER_READ_WRITE
);
} 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 => {
// 0. Filter out Gemini tools if noGemini is set
if (CONFIG.noGemini && GEMINI_TOOLS.includes(tool.name)) {
return false;
}
// 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;
}
}