Skip to main content
Glama
dacebt

Prompt Cleaner MCP Server

by dacebt
cleaner.ts5.55 kB
import { promises as fs } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { chatCompletions } from "./llm.js"; import { RetouchInputT, RetouchOutput, RetouchOutputT } from "./shapes.js"; import { ensureNoSecretsInObject, redactSecrets } from "./redact.js"; import { config } from "./config.js"; import { logger } from "./log.js"; let cachedPrompt: string | null = null; async function loadCleanerSystemPrompt(): Promise<string> { if (cachedPrompt) return cachedPrompt; const here = dirname(fileURLToPath(import.meta.url)); // Load cleaner prompt only (legacy retoucher prompt removed) const cleanerPath = join(here, "..", "prompts", "cleaner.md"); const buf = await fs.readFile(cleanerPath, "utf8"); cachedPrompt = buf; return buf; } function extractFirstJsonObject(text: string): any { // Remove common code fences (```json ... ``` or ``` ... ```) const unfenced = text .replace(/```[a-zA-Z]*\n?/g, "") .replace(/```/g, "") .trim(); // Fast path: try parsing the whole string try { return JSON.parse(unfenced); } catch {} // Scan for the first balanced JSON object while ignoring braces inside strings const s = unfenced; let start = -1; let depth = 0; let inString = false; let escape = false; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (inString) { if (escape) { escape = false; continue; } if (ch === "\\") { escape = true; continue; } if (ch === '"') { inString = false; } continue; } if (ch === '"') { inString = true; continue; } if (ch === "{") { if (depth === 0) start = i; depth++; continue; } if (ch === "}") { if (depth > 0) { depth--; if (depth === 0 && start !== -1) { const candidate = s.slice(start, i + 1); try { return JSON.parse(candidate); } catch { // keep scanning; there may be another valid object ahead start = -1; } } } } } throw new Error("Cleaner returned non-JSON"); } export async function retouchPrompt(input: RetouchInputT): Promise<RetouchOutputT> { const start = Date.now(); const system = await loadCleanerSystemPrompt(); const baseTemperature = input.temperature ?? 0; const userBody = `MODE: ${input.mode || "general"}\nRAW_PROMPT:\n${input.prompt}`; const sys = system; class CleanerNonJsonError extends Error { constructor(message = "Cleaner returned non-JSON") { super(message); this.name = "CleanerNonJsonError"; } } // Retry loop for content-level non-JSON responses const maxAttempts = Math.max(1, 1 + (config.contentMaxRetries ?? 0)); let lastErr: Error | undefined; for (let attempt = 1; attempt <= maxAttempts; attempt++) { // Escalate on retries: enforce temperature 0 and stricter system instructions const strictSuffix = attempt > 1 ? "\n\nSTRICT OUTPUT MODE: Respond with EXACTLY ONE JSON object and nothing else. No prose. No code fences. No prefix/suffix." : ""; const sysAttempt = sys + strictSuffix; const tempAttempt = attempt > 1 ? 0 : baseTemperature; const response = await chatCompletions( { model: config.model, temperature: tempAttempt, max_tokens: 600, messages: [ { role: "system", content: sysAttempt }, { role: "user", content: userBody }, ], }, { requestId: input.requestId }, ); const content = response.choices?.[0]?.message?.content ?? ""; const initial = redactSecrets(content); const redactedText = initial.text; try { const obj = extractFirstJsonObject(redactedText); const parsed = RetouchOutput.safeParse(obj); if (!parsed.success) { throw new Error("shape-error"); } const { value, redactions } = ensureNoSecretsInObject(parsed.data); const totalRedactions = initial.count + redactions; const result: RetouchOutputT = { ...value, redactions: totalRedactions > 0 ? Array(totalRedactions).fill("[REDACTED]") : value.redactions, }; logger.info("retouch.prompt", { elapsed_ms: Date.now() - start, input_len: input.prompt.length, preview: logger.preview(input.prompt), request_id: input.requestId, attempts: attempt, outcome: "ok", }); return result; } catch (e: any) { lastErr = new CleanerNonJsonError("Cleaner returned non-JSON"); if (attempt < maxAttempts) { const base = config.backoffMs ?? 250; const jitter = config.backoffJitter ?? 0.2; const exp = Math.pow(2, attempt - 1); const rand = 1 + (Math.random() * 2 - 1) * jitter; // 1 +/- jitter const delay = Math.max(0, Math.floor(base * exp * rand)); logger.warn("retouch.retry", { request_id: input.requestId, attempt, delay_ms: delay, reason: "non-json", }); await new Promise((r) => setTimeout(r, delay)); continue; } } } logger.info("retouch.prompt", { elapsed_ms: Date.now() - start, input_len: input.prompt.length, preview: logger.preview(input.prompt), request_id: input.requestId, attempts: maxAttempts, outcome: "error", reason: "non-json", }); throw lastErr ?? new CleanerNonJsonError("Cleaner returned non-JSON"); }

Implementation Reference

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/dacebt/prompt-cleaner-mcp'

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