Skip to main content
Glama
config.ts6.45 kB
import { existsSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { isAbsolute, join, resolve } from "node:path"; import { ERROR_CODES } from "@/packages/common/errors/error-codes"; import { HTTP_STATUS } from "@/packages/common/errors/status-codes"; import { ConfigError } from "./errors"; import { DATA_DIR_ENV_KEY } from "./constants"; import { appStoreSchema, playStoreSchema } from "./schemas"; import type { EnvConfig } from "./types"; // Config paths const CONFIG_DIR = join(homedir(), ".config", "pabal-mcp"); const CONFIG_PATH = join(CONFIG_DIR, "config.json"); // Security: recommended permissions const RECOMMENDED_DIR_MODE = 0o700; // drwx------ const RECOMMENDED_FILE_MODE = 0o600; // -rw------- /** * Check file/directory permissions and warn if too permissive */ function checkPermissions(path: string, isDirectory: boolean): void { try { const stats = statSync(path); const mode = stats.mode & 0o777; const recommended = isDirectory ? RECOMMENDED_DIR_MODE : RECOMMENDED_FILE_MODE; // Check if "group" or "others" have any permissions const groupOthersPerms = mode & 0o077; if (groupOthersPerms !== 0) { const typeLabel = isDirectory ? "directory" : "file"; const recommendedStr = recommended.toString(8); console.error( `[Security] ⚠️ Warning: ${typeLabel} "${path}" has permissive permissions (${mode.toString(8)})` ); console.error(`[Security] Run: chmod ${recommendedStr} "${path}"`); } } catch { // Ignore permission check errors } } /** * Check all config files for secure permissions */ function checkConfigSecurity(configDir: string): void { // Check directory permissions checkPermissions(configDir, true); // Check all files in config directory const sensitiveFiles = [ "config.json", "app-store-key.p8", "google-play-service-account.json", "registered-apps.json", ]; for (const file of sensitiveFiles) { const filePath = join(configDir, file); if (existsSync(filePath)) { checkPermissions(filePath, false); } } } /** * Get config directory path: ~/.config/pabal-mcp/ */ export function getConfigDir(): string { return CONFIG_DIR; } /** * Get config file path: ~/.config/pabal-mcp/config.json */ export function getConfigPath(): string { return CONFIG_PATH; } export function readConfigFile(): any { const configPath = getConfigPath(); if (!existsSync(configPath)) { return null; } try { const configContent = readFileSync(configPath, "utf-8"); return JSON.parse(configContent); } catch (error) { console.error( `[Config] ⚠️ Failed to read config file: ${ error instanceof Error ? error.message : String(error) }` ); return null; } } export function getDataDir(): string { const configDir = getConfigDir(); console.error(`[Config] 🔍 Checking data directory config...`); // 1. Check config file first const config = readConfigFile(); if (config?.dataDir && typeof config.dataDir === "string") { const normalized = config.dataDir.trim(); if (normalized) { const resultDir = isAbsolute(normalized) ? normalized : resolve(configDir, normalized); console.error(`[Config] ✅ Using config file dataDir: ${resultDir}`); return resultDir; } } // 2. Check environment variable const override = process.env[DATA_DIR_ENV_KEY]; if (override && override.trim()) { const normalized = override.trim(); const resultDir = isAbsolute(normalized) ? normalized : resolve(configDir, normalized); console.error(`[Config] ✅ Using environment variable: ${resultDir}`); return resultDir; } // 3. Default to config directory console.error(`[Config] ✅ Using default (config dir): ${configDir}`); return configDir; } export function loadConfig(): EnvConfig { const configDir = getConfigDir(); // Security check: warn if permissions are too permissive checkConfigSecurity(configDir); const config = readConfigFile(); if (!config) { const configPath = getConfigPath(); throw new ConfigError( `Config file not found: ${configPath}\n` + `Please create ~/.config/pabal-mcp/config.json`, { status: HTTP_STATUS.NOT_FOUND, code: ERROR_CODES.CONFIG_NOT_FOUND } ); } try { const result: EnvConfig = {}; if (config.appStore) { const { issuerId, keyId, privateKeyPath } = config.appStore; if (issuerId && keyId && privateKeyPath) { const keyPath = isAbsolute(privateKeyPath) ? privateKeyPath : resolve(configDir, privateKeyPath); const privateKey = readFileSafe(keyPath); if (privateKey) { result.appStore = appStoreSchema.parse({ keyId, issuerId, privateKey: normalizePrivateKey(privateKey), }); } } } if (config.googlePlay?.serviceAccountKeyPath) { const serviceAccountPath = config.googlePlay.serviceAccountKeyPath; const jsonPath = isAbsolute(serviceAccountPath) ? serviceAccountPath : resolve(configDir, serviceAccountPath); const json = readFileSafe(jsonPath); if (json) { result.playStore = playStoreSchema.parse({ serviceAccountJson: json, }); } } if (!result.appStore && !result.playStore) { throw new ConfigError( "Config file does not contain App Store or Play Store authentication information.", { status: HTTP_STATUS.UNAUTHORIZED, code: ERROR_CODES.CONFIG_AUTH_NOT_FOUND, } ); } return result; } catch (error) { if (error instanceof ConfigError) { throw error; } throw new ConfigError( `Error reading config file: ${ error instanceof Error ? error.message : String(error) }`, { status: HTTP_STATUS.INTERNAL_SERVER_ERROR, code: ERROR_CODES.CONFIG_READ_FAILED, cause: error, } ); } } function readFileSafe(path: string): string | undefined { try { return readFileSync(path, "utf-8"); } catch { return undefined; } } // Restore line breaks in PEM key function normalizePrivateKey(raw: string): string { if (raw.includes("-----BEGIN")) { return raw; } const restored = raw.replace(/\\n/g, "\n"); return restored; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quartz-labs-dev/pabal-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server