Skip to main content
Glama
trpc-client.ts12.8 kB
import axios, { AxiosInstance } from "axios"; import { CONFIG } from "./config.js"; import { NextAuthHandler } from "./nextauth.js"; import { logger } from "./logger.js"; import { AxiosError, NetworkError } from "./types.js"; export interface TRPCRequest { id: number; method: string; params?: unknown; } export interface TRPCResponse<T = unknown> { id: number; result?: T; error?: { code: number; message: string; data?: unknown; }; } export type TRPCBatchRequest = Array<TRPCRequest>; export type TRPCBatchResponse = Array<TRPCResponse>; export class TRPCClient { private httpClient: AxiosInstance; private nextAuth: NextAuthHandler; private requestId: number = 1; private allCookies: string = ""; constructor(nextAuth: NextAuthHandler) { this.nextAuth = nextAuth; this.httpClient = axios.create({ baseURL: `${CONFIG.nlobby.baseUrl}/api/trpc`, timeout: 15000, headers: { "Content-Type": "application/json", "User-Agent": CONFIG.userAgent, Accept: "application/json", "Accept-Language": "ja,en-US;q=0.7,en;q=0.3", "Accept-Encoding": "gzip, deflate, br", Connection: "keep-alive", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", }, withCredentials: true, }); this.setupInterceptors(); } setAllCookies(cookies: string): void { if (!cookies || cookies.trim() === "") { logger.warn("[WARNING] Empty cookies provided to tRPC client"); this.allCookies = ""; return; } this.allCookies = cookies; logger.info("[SUCCESS] tRPC client cookies updated"); logger.info(`[SIZE] tRPC cookie string length: ${cookies.length}`); // Verify cookies are properly set if (this.allCookies === cookies) { logger.info("[SUCCESS] tRPC cookie verification successful"); } else { logger.error("[ERROR] tRPC cookie verification failed"); } } private setupInterceptors(): void { this.httpClient.interceptors.request.use((config) => { // Priority: Use all cookies if available, otherwise fall back to NextAuth cookies let cookieHeader: string | undefined; if (this.allCookies && this.allCookies.trim() !== "") { cookieHeader = this.allCookies; logger.debug("[COOKIE] Using all cookies for tRPC request"); } else { // Fallback to NextAuth.js cookies only const nextAuthCookieHeader = this.nextAuth.getCookieHeader(); if (nextAuthCookieHeader && nextAuthCookieHeader.trim() !== "") { cookieHeader = nextAuthCookieHeader; logger.debug("[COOKIE] Using NextAuth cookies for tRPC request"); } else { logger.warn("[WARNING] No cookies available for tRPC request"); } } // Set cookie header if we have cookies if (cookieHeader) { config.headers["Cookie"] = cookieHeader; } // Add Authorization header with NextAuth session token const sessionToken = this.nextAuth.getSessionToken(); if (sessionToken) { config.headers["Authorization"] = `Bearer ${sessionToken}`; logger.debug("Added Authorization header with session token"); } else { logger.warn( "[WARNING] No session token available for Authorization header", ); } // Add CSRF token to headers if available const csrfToken = this.nextAuth.getCookies().csrfToken; if (csrfToken) { config.headers["X-CSRF-Token"] = csrfToken; logger.debug("Added CSRF token to tRPC request"); } // Log request details for debugging logger.debug("[REQUEST] tRPC request details:", { url: config.url, method: config.method?.toUpperCase(), hasCookies: !!cookieHeader, hasCSRF: !!csrfToken, hasAuth: !!sessionToken, cookieSource: this.allCookies ? "allCookies" : "nextAuth", }); return config; }); this.httpClient.interceptors.response.use( (response) => { logger.debug("[SUCCESS] tRPC response received:", { status: response.status, statusText: response.statusText, hasData: !!response.data, }); return response; }, async (error) => { logger.error("[ERROR] tRPC request failed:", { status: error.response?.status, statusText: error.response?.statusText, message: error.message, url: error.config?.url, }); if (error.response?.status === 401) { logger.error( "[BLOCKED] Authentication failed - NextAuth session may be expired", ); throw new Error( "Authentication expired. Please re-authenticate with NextAuth cookies.", ); } else if (error.response?.status === 403) { logger.error("[BLOCKED] Access forbidden - insufficient permissions"); throw new Error( "Access forbidden. Check your permissions or re-authenticate.", ); } else if (error.response?.status === 404) { logger.error("[BLOCKED] tRPC endpoint not found"); throw new Error("tRPC endpoint not found. The API may have changed."); } return Promise.reject(error); }, ); } private getNextRequestId(): number { return this.requestId++; } async call<T = unknown>(method: string, params?: unknown): Promise<T> { const request: TRPCRequest = { id: this.getNextRequestId(), method, params, }; try { logger.info( `[REQUEST] tRPC call: ${method}`, params ? `with params: ${JSON.stringify(params)}` : "without params", ); // Log request headers for debugging const cookieHeader = this.allCookies || this.nextAuth.getCookieHeader(); logger.debug( `[COOKIE] Request cookies: ${cookieHeader ? "present" : "missing"}`, ); // Try GET approach first (query-based tRPC) logger.debug("Trying GET approach..."); try { const url = this.buildTRPCUrl(method, params); logger.debug(`[URL] tRPC GET URL: ${url}`); const getResponse = await this.httpClient.get<TRPCResponse<T>>(url); logger.debug( `[SUCCESS] tRPC ${method} GET response status: ${getResponse.status}`, ); if (getResponse.data.error) { logger.error( `[ERROR] tRPC ${method} GET returned error:`, getResponse.data.error, ); throw new Error( `tRPC Error [${getResponse.data.error.code}]: ${getResponse.data.error.message}`, ); } logger.debug(`[SUCCESS] tRPC ${method} GET succeeded`); return getResponse.data.result as T; } catch (getError) { logger.debug(`[WARNING] GET approach failed, trying POST approach...`); logger.debug( `[DEBUG] GET error details:`, getError instanceof Error ? getError.message : "Unknown error", ); // Try POST approach (JSON-RPC style) const postUrl = method; logger.debug(`[URL] tRPC POST URL: ${postUrl}`); const postResponse = await this.httpClient.post<TRPCResponse<T>>( postUrl, request, { headers: { "Content-Type": "application/json", }, }, ); logger.debug( `[SUCCESS] tRPC ${method} POST response status: ${postResponse.status}`, ); if (postResponse.data.error) { logger.error( `[ERROR] tRPC ${method} POST returned error:`, postResponse.data.error, ); throw new Error( `tRPC Error [${postResponse.data.error.code}]: ${postResponse.data.error.message}`, ); } logger.debug(`[SUCCESS] tRPC ${method} POST succeeded`); return postResponse.data.result as T; } } catch (error) { logger.error(`[ERROR] tRPC call failed for ${method}:`, { message: error instanceof Error ? error.message : "Unknown error", stack: error instanceof Error ? error.stack : undefined, }); // Enhanced axios error logging if (error && typeof error === "object" && "response" in error) { const axiosError = error as AxiosError; logger.error(`[DEBUG] tRPC ${method} Axios error details:`, { status: axiosError.response?.status, statusText: axiosError.response?.statusText, headers: axiosError.response?.headers, data: axiosError.response?.data, url: axiosError.config?.url, method: axiosError.config?.method, timeout: axiosError.config?.timeout, cookies: axiosError.config?.headers?.Cookie ? "present" : "missing", }); // Check for specific error types if (axiosError.response?.status === 401) { logger.error( "[BLOCKED] tRPC 401 Unauthorized - session may be expired or invalid", ); } else if (axiosError.response?.status === 403) { logger.error( "[BLOCKED] tRPC 403 Forbidden - insufficient permissions", ); } else if (axiosError.response?.status === 404) { logger.error("[BLOCKED] tRPC 404 Not Found - endpoint may not exist"); } else if ( axiosError.response?.status && axiosError.response.status >= 500 ) { logger.error("[BLOCKED] tRPC Server Error - N Lobby backend issue"); } } else if (error && typeof error === "object" && "code" in error) { const networkError = error as NetworkError; if (networkError.code === "ECONNREFUSED") { logger.error( "[NETWORK] Network Error: Connection refused - N Lobby may be down", ); } else if (networkError.code === "ETIMEDOUT") { logger.error( "[TIMEOUT] Network Error: Request timeout - slow network or server overload", ); } else if (networkError.code === "ENOTFOUND") { logger.error( "[NETWORK] Network Error: DNS lookup failed - check internet connection", ); } } throw error; } } private buildTRPCUrl(method: string, params?: unknown): string { // tRPC URL format: /api/trpc/method.name?input={"param":"value"} // The baseURL already includes /api/trpc, so we just need the method name const methodUrl = method; // Always include input parameter, even if empty const input = params ? JSON.stringify(params) : JSON.stringify({}); const queryString = new URLSearchParams({ input: input, }).toString(); return `${methodUrl}?${queryString}`; } // Specific methods for N Lobby API endpoints async getUnreadNewsCount(): Promise<number> { return this.call<number>("news.getUnreadNewsCount"); } async getNotificationMessages(): Promise<unknown[]> { return this.call<unknown[]>("notification.getMessages"); } async updateLastAccess(): Promise<void> { return this.call<void>("user.updateLastAccess"); } async findMainNavigations(): Promise<unknown[]> { return this.call<unknown[]>("menu.findMainNavigations", {}); } async readInterestsWithIcon(): Promise<unknown[]> { return this.call<unknown[]>("interest.readInterestsWithIcon"); } async readInterests(): Promise<unknown[]> { return this.call<unknown[]>("interest.readInterests"); } async readWeights(): Promise<unknown[]> { return this.call<unknown[]>("interest.readWeights"); } async getLobbyCalendarEvents(from: string, to: string): Promise<unknown[]> { return this.call<unknown[]>("calendar.getLobbyCalendarEvents", { from, to, }); } // Health check method async healthCheck(): Promise<boolean> { logger.info("Running tRPC health check..."); // Try multiple endpoints to verify connection const healthCheckMethods = [ { name: "updateLastAccess", method: () => this.updateLastAccess() }, { name: "getUnreadNewsCount", method: () => this.getUnreadNewsCount() }, { name: "findMainNavigations", method: () => this.findMainNavigations() }, ]; for (const { name, method } of healthCheckMethods) { try { logger.debug(`Trying tRPC method: ${name}`); await method(); logger.info(`[SUCCESS] tRPC health check passed with method: ${name}`); return true; } catch (error) { logger.debug( `[ERROR] tRPC method ${name} failed:`, error instanceof Error ? error.message : "Unknown error", ); continue; } } logger.error("[ERROR] All tRPC health check methods failed"); return false; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/minagishl/nlobby-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server