mcp-quiz-server
Version:
🧠AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.
201 lines • 6.45 kB
JavaScript
import { AuthService } from './AuthService';
export class ApiClient {
constructor(baseURL = '') {
this.baseURL = baseURL;
this.authService = AuthService.getInstance();
}
static getInstance() {
if (!ApiClient.instance) {
ApiClient.instance = new ApiClient();
}
return ApiClient.instance;
}
async get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
async post(url, data, config = {}) {
return this.request(url, { ...config, method: 'POST', data });
}
async put(url, data, config = {}) {
return this.request(url, { ...config, method: 'PUT', data });
}
async delete(url, config = {}) {
return this.request(url, { ...config, method: 'DELETE' });
}
async patch(url, data, config = {}) {
return this.request(url, { ...config, method: 'PATCH', data });
}
async request(url, config) {
const fullURL = this.buildURL(url);
const requestConfig = await this.buildRequestConfig(config);
try {
const response = await fetch(fullURL, requestConfig);
return await this.handleResponse(response);
}
catch (error) {
throw this.handleError(error);
}
}
buildURL(url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
const base = this.baseURL || window.location.origin;
const cleanBase = base.replace(/\/$/, '');
const cleanPath = url.replace(/^\//, '');
return `${cleanBase}/${cleanPath}`;
}
async buildRequestConfig(config) {
const headers = new Headers(config.headers);
if (!config.skipAuth) {
const authHeader = this.authService.getAuthHeader();
if (authHeader) {
headers.set('Authorization', authHeader);
}
}
if (config.data && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
const requestConfig = {
method: config.method || 'GET',
headers,
credentials: 'include',
};
if (config.data) {
if (config.data instanceof FormData) {
requestConfig.body = config.data;
headers.delete('Content-Type');
}
else {
requestConfig.body = JSON.stringify(config.data);
}
}
if (config.signal) {
requestConfig.signal = config.signal;
}
return requestConfig;
}
async handleResponse(response) {
const contentType = response.headers.get('Content-Type') || '';
const isJSON = contentType.includes('application/json');
let data;
try {
if (isJSON) {
data = await response.json();
}
else {
data = await response.text();
}
}
catch (error) {
data = null;
}
if (response.status === 401) {
await this.handleUnauthorized();
throw new ApiError('Authentication required', 401, data);
}
if (!response.ok) {
const message = this.extractErrorMessage(data);
throw new ApiError(message, response.status, data);
}
return {
data,
status: response.status,
statusText: response.statusText,
headers: this.responseHeadersToObject(response.headers),
};
}
async handleUnauthorized() {
try {
await this.authService.refreshToken();
}
catch (error) {
await this.authService.logout();
if (!window.location.pathname.includes('/auth/')) {
window.location.href = '/auth/login.html';
}
}
}
extractErrorMessage(data) {
if (!data)
return 'Request failed';
if (typeof data === 'string') {
return data;
}
if (data.error) {
if (typeof data.error === 'string') {
return data.error;
}
if (data.error.message) {
return data.error.message;
}
}
if (data.message) {
return data.message;
}
return 'Request failed';
}
handleError(error) {
if (error instanceof ApiError) {
return error;
}
if (error.name === 'AbortError') {
return new ApiError('Request cancelled', 0, error);
}
if (error.name === 'TypeError' && error.message.includes('fetch')) {
return new ApiError('Network error. Please check your connection.', 0, error);
}
return new ApiError(error.message || 'An unexpected error occurred', 0, error);
}
responseHeadersToObject(headers) {
const result = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}
async requestWithTimeout(url, config, timeout = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await this.request(url, {
...config,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
}
catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
async uploadFile(url, file, config = {}) {
const formData = new FormData();
formData.append(config.fieldName || 'file', file);
if (config.fields) {
Object.entries(config.fields).forEach(([key, value]) => {
formData.append(key, value);
});
}
return this.post(url, formData, {
skipAuth: config.skipAuth,
headers: config.headers,
});
}
setBaseURL(baseURL) {
this.baseURL = baseURL;
}
getBaseURL() {
return this.baseURL;
}
}
export class ApiError extends Error {
constructor(message, status = 0, data = null) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
//# sourceMappingURL=ApiClient.js.map