mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-18 19:41:31 +00:00
- Add provider registry to manage LLM providers (Claude, Codex, Cursor, Gemini). - Create provider routes for MCP server operations (list, upsert, delete, run). - Implement MCP service for handling server operations and validations. - Introduce abstract provider class and MCP provider base for shared functionality. - Add tests for MCP server operations across different providers and scopes. - Define shared interfaces and types for MCP functionality. - Implement utility functions for handling JSON config files and API responses.
167 lines
5.2 KiB
TypeScript
167 lines
5.2 KiB
TypeScript
|
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
|
|
import type { ApiErrorShape, ApiSuccessShape, AppErrorOptions } from '@/shared/types.js';
|
|
|
|
export function createApiSuccessResponse<TData>(
|
|
data: TData,
|
|
): ApiSuccessShape<TData> {
|
|
return {
|
|
success: true,
|
|
data,
|
|
};
|
|
}
|
|
|
|
export function createApiErrorResponse(
|
|
code: string,
|
|
message: string,
|
|
details?: unknown
|
|
): ApiErrorShape {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code,
|
|
message,
|
|
details,
|
|
}
|
|
};
|
|
}
|
|
|
|
export function asyncHandler(
|
|
handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
|
|
): RequestHandler {
|
|
return (req, res, next) => {
|
|
void Promise.resolve(handler(req, res, next)).catch(next);
|
|
};
|
|
}
|
|
|
|
// --------- Global app error class for consistent error handling across the server ---------
|
|
export class AppError extends Error {
|
|
readonly code: string;
|
|
readonly statusCode: number;
|
|
readonly details?: unknown;
|
|
|
|
constructor(message: string, options: AppErrorOptions = {}) {
|
|
super(message);
|
|
this.name = 'AppError';
|
|
this.code = options.code ?? 'INTERNAL_ERROR';
|
|
this.statusCode = options.statusCode ?? 500;
|
|
this.details = options.details;
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------
|
|
|
|
// ------------------------ The following are mainly for provider MCP runtimes ------------------------
|
|
/**
|
|
* Safely narrows an unknown value to a plain object record.
|
|
*
|
|
* This deliberately rejects arrays, `null`, and primitive values so callers can
|
|
* treat the returned value as a JSON-style object map without repeating the same
|
|
* defensive shape checks at every config read site.
|
|
*/
|
|
export const readObjectRecord = (value: unknown): Record<string, unknown> | null => {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
|
|
return value as Record<string, unknown>;
|
|
};
|
|
|
|
/**
|
|
* Reads an optional string from unknown input and normalizes empty or whitespace-only
|
|
* values to `undefined`.
|
|
*
|
|
* This is useful when parsing config files where a field may be missing, present
|
|
* with the wrong type, or present as an empty string that should be treated as
|
|
* "not configured".
|
|
*/
|
|
export const readOptionalString = (value: unknown): string | undefined => {
|
|
if (typeof value !== 'string') {
|
|
return undefined;
|
|
}
|
|
|
|
const normalized = value.trim();
|
|
return normalized.length > 0 ? normalized : undefined;
|
|
};
|
|
|
|
/**
|
|
* Reads an optional string array from unknown input.
|
|
*
|
|
* Non-array values are ignored, and any array entries that are not strings are
|
|
* filtered out. This lets provider config readers consume loosely shaped JSON/TOML
|
|
* data without failing on incidental invalid members.
|
|
*/
|
|
export const readStringArray = (value: unknown): string[] | undefined => {
|
|
if (!Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
return value.filter((entry): entry is string => typeof entry === 'string');
|
|
};
|
|
|
|
/**
|
|
* Reads an optional string-to-string map from unknown input.
|
|
*
|
|
* The function first ensures the source value is a plain object, then keeps only
|
|
* keys whose values are strings. If no valid entries remain, it returns `undefined`
|
|
* so callers can distinguish "no usable map" from an empty object that was
|
|
* intentionally authored downstream.
|
|
*/
|
|
export const readStringRecord = (value: unknown): Record<string, string> | undefined => {
|
|
const record = readObjectRecord(value);
|
|
if (!record) {
|
|
return undefined;
|
|
}
|
|
|
|
const normalized: Record<string, string> = {};
|
|
for (const [key, entry] of Object.entries(record)) {
|
|
if (typeof entry === 'string') {
|
|
normalized[key] = entry;
|
|
}
|
|
}
|
|
|
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
};
|
|
|
|
/**
|
|
* Reads a JSON config file and guarantees a plain object result.
|
|
*
|
|
* Missing files are treated as an empty config object so provider-specific MCP
|
|
* readers can operate against first-run environments without special-case file
|
|
* existence checks. If the file exists but contains invalid JSON, the parse error
|
|
* is preserved and rethrown.
|
|
*/
|
|
export const readJsonConfig = async (filePath: string): Promise<Record<string, unknown>> => {
|
|
try {
|
|
const content = await readFile(filePath, 'utf8');
|
|
const parsed = JSON.parse(content) as Record<string, unknown>;
|
|
return readObjectRecord(parsed) ?? {};
|
|
} catch (error) {
|
|
const code = (error as NodeJS.ErrnoException).code;
|
|
if (code === 'ENOENT') {
|
|
return {};
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Writes a JSON config file with stable, human-readable formatting.
|
|
*
|
|
* The parent directory is created automatically so callers can persist config into
|
|
* provider-specific folders without pre-creating the directory tree. Output always
|
|
* ends with a trailing newline to keep the file diff-friendly.
|
|
*/
|
|
export const writeJsonConfig = async (filePath: string, data: Record<string, unknown>): Promise<void> => {
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
};
|
|
|
|
// -------------------------------------------------------------------------------------------
|
|
|