* Internal utility: Write MCP config to .mcp.json file. * Preserves file permissions and flushes to disk before rename. * Uses the original path for rename (does not follow symlinks).
(config: McpJsonConfig)
| 86 | * Uses the original path for rename (does not follow symlinks). |
| 87 | */ |
| 88 | async function writeMcpjsonFile(config: McpJsonConfig): Promise<void> { |
| 89 | const mcpJsonPath = join(getCwd(), '.mcp.json') |
| 90 | |
| 91 | // Read existing file permissions to preserve them |
| 92 | let existingMode: number | undefined |
| 93 | try { |
| 94 | const stats = await stat(mcpJsonPath) |
| 95 | existingMode = stats.mode |
| 96 | } catch (e: unknown) { |
| 97 | const code = getErrnoCode(e) |
| 98 | if (code !== 'ENOENT') { |
| 99 | throw e |
| 100 | } |
| 101 | // File doesn't exist yet -- no permissions to preserve |
| 102 | } |
| 103 | |
| 104 | // Write to temp file, flush to disk, then atomic rename |
| 105 | const tempPath = `${mcpJsonPath}.tmp.${process.pid}.${Date.now()}` |
| 106 | const handle = await open(tempPath, 'w', existingMode ?? 0o644) |
| 107 | try { |
| 108 | await handle.writeFile(jsonStringify(config, null, 2), { |
| 109 | encoding: 'utf8', |
| 110 | }) |
| 111 | await handle.datasync() |
| 112 | } finally { |
| 113 | await handle.close() |
| 114 | } |
| 115 | |
| 116 | try { |
| 117 | // Restore original file permissions on the temp file before rename |
| 118 | if (existingMode !== undefined) { |
| 119 | await chmod(tempPath, existingMode) |
| 120 | } |
| 121 | await rename(tempPath, mcpJsonPath) |
| 122 | } catch (e: unknown) { |
| 123 | // Clean up temp file on failure |
| 124 | try { |
| 125 | await unlink(tempPath) |
| 126 | } catch { |
| 127 | // Best-effort cleanup |
| 128 | } |
| 129 | throw e |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Extract command array from server config (stdio servers only) |
no test coverage detected