mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-18 19:41:31 +00:00
feat: implement MCP provider registry and service
- 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.
This commit is contained in:
166
server/shared/utils.ts
Normal file
166
server/shared/utils.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
|
||||
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');
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user