Skip to main content
Glama
policy.ts3.43 kB
import os from "node:os"; import path from "node:path"; import fs from "node:fs/promises"; export type PolicyDecision = { ok: true } | { ok: false; reason: string }; function homeDir(): string { return process.env.HOME ?? os.homedir(); } function auditLogPath(): string { return path.join(homeDir(), ".mcpmanager", "audit.log"); } function redact(value: unknown): unknown { if (Array.isArray(value)) return value.map((v) => redact(v)); if (value && typeof value === "object") { const obj = value as Record<string, unknown>; const out: Record<string, unknown> = {}; for (const [k, v] of Object.entries(obj)) { if (/key|token|secret|password/i.test(k)) out[k] = "*****"; else out[k] = redact(v); } return out; } return value; } export async function audit(toolName: string, args: unknown, decision: PolicyDecision) { const line = JSON.stringify( { ts: new Date().toISOString(), tool: toolName, args: redact(args), decision, }, null, 0, ); const p = auditLogPath(); await fs.mkdir(path.dirname(p), { recursive: true }); await fs.appendFile(p, line + "\n", "utf8"); } function truthy(v: string | undefined): boolean { return v === "1" || v?.toLowerCase() === "true" || v?.toLowerCase() === "yes"; } export function checkPolicy(toolName: string, args: any): PolicyDecision { const mode = (process.env.MCPMANAGER_POLICY_MODE ?? "strict").toLowerCase(); if (mode === "permissive") return { ok: true }; // Safe-by-default: block generating Tailscale auth keys unless explicitly enabled. if (toolName === "tailscale.keys.createEphemeral") { if (!truthy(process.env.MCPMANAGER_TAILSCALE_ALLOW_KEYS)) { return { ok: false, reason: "Denied by policy: set MCPMANAGER_TAILSCALE_ALLOW_KEYS=1 to allow tailscale key creation.", }; } const maxExpiry = Number(process.env.MCPMANAGER_TAILSCALE_MAX_EXPIRY_SECONDS ?? "3600"); const expiry = Number(args?.expirySeconds ?? 0); if (!Number.isFinite(expiry) || expiry <= 0 || expiry > maxExpiry) { return { ok: false, reason: `Denied by policy: expirySeconds must be 1..${maxExpiry}.` }; } const reusableAllowed = truthy(process.env.MCPMANAGER_TAILSCALE_ALLOW_REUSABLE); if (args?.reusable === true && !reusableAllowed) { return { ok: false, reason: "Denied by policy: reusable keys are not allowed." }; } if (args?.ephemeral !== true) { return { ok: false, reason: "Denied by policy: ephemeral must be true." }; } const lockTailnet = process.env.MCPMANAGER_TAILSCALE_TAILNET_LOCK ?? process.env.TAILSCALE_TAILNET; if (lockTailnet && args?.tailnet && args.tailnet !== lockTailnet) { return { ok: false, reason: "Denied by policy: tailnet override is not allowed." }; } const allowedTags = (process.env.MCPMANAGER_TAILSCALE_TAGS_ALLOW ?? "") .split(",") .map((s) => s.trim()) .filter(Boolean); const tags: unknown = args?.tags; if (!Array.isArray(tags)) { return { ok: false, reason: "Denied by policy: tags must be an array." }; } if (allowedTags.length > 0) { for (const tag of tags) { if (typeof tag !== "string" || !allowedTags.includes(tag)) { return { ok: false, reason: "Denied by policy: tags not in allowlist." }; } } } return { ok: true }; } return { ok: true }; }

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/maxtheman/mcpManager'

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