Add shared JSON file helpers

This commit is contained in:
Peter Steinberger 2025-11-07 04:24:24 +00:00
parent a1fc269911
commit 50f32e35ab
3 changed files with 57 additions and 19 deletions

21
src/fs-json.ts Normal file
View File

@ -0,0 +1,21 @@
import fs from 'node:fs/promises';
import path from 'node:path';
// readJsonFile reads a JSON file and returns undefined when the file does not exist.
export async function readJsonFile<T = unknown>(filePath: string): Promise<T | undefined> {
try {
const content = await fs.readFile(filePath, 'utf8');
return JSON.parse(content) as T;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return undefined;
}
throw error;
}
}
// writeJsonFile writes a JSON object to disk, ensuring parent directories are created first.
export async function writeJsonFile(filePath: string, data: unknown): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
}

View File

@ -12,6 +12,7 @@ import type {
OAuthTokens,
} from '@modelcontextprotocol/sdk/shared/auth.js';
import type { ServerDefinition } from './config.js';
import { readJsonFile, writeJsonFile } from './fs-json.js';
const CALLBACK_HOST = '127.0.0.1';
const CALLBACK_PATH = '/callback';
@ -61,25 +62,6 @@ async function ensureDirectory(dir: string) {
await fs.mkdir(dir, { recursive: true });
}
// readJsonFile returns undefined for missing files instead of throwing.
async function readJsonFile<T>(filePath: string): Promise<T | undefined> {
try {
const raw = await fs.readFile(filePath, 'utf8');
return JSON.parse(raw) as T;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return undefined;
}
throw error;
}
}
// writeJsonFile persists JSON data to disk, creating parent directories as needed.
async function writeJsonFile(filePath: string, data: unknown) {
await ensureDirectory(path.dirname(filePath));
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
}
// FileOAuthClientProvider persists OAuth session artifacts to disk and captures callback redirects.
class FileOAuthClientProvider implements OAuthClientProvider {
private readonly tokenPath: string;

35
tests/fs-json.test.ts Normal file
View File

@ -0,0 +1,35 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { readJsonFile, writeJsonFile } from '../src/fs-json.js';
describe('fs-json helpers', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-fs-json-'));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('returns undefined when reading a missing file', async () => {
const missingPath = path.join(tempDir, 'missing.json');
const value = await readJsonFile<Record<string, string>>(missingPath);
expect(value).toBeUndefined();
});
it('writes JSON and reads it back, ensuring parent directories are created', async () => {
const nestedPath = path.join(tempDir, 'nested', 'config.json');
const payload = { apiKey: 'secret', retries: 2 };
await writeJsonFile(nestedPath, payload);
const roundTripped = await readJsonFile<typeof payload>(nestedPath);
expect(roundTripped).toEqual(payload);
const raw = await fs.readFile(nestedPath, 'utf8');
expect(raw).toContain('\n "apiKey"');
});
});