Skip to main content
Glama
hiltonbrown

Next.js MCP Server Template

by hiltonbrown
xero-client.ts8.76 kB
// Xero API client setup import { XeroClient } from 'xero-node'; import { db } from './db'; import { refreshAccessToken } from './auth'; import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; // Custom error class for token refresh failures export class TokenRefreshError extends Error { constructor(message: string, public originalError?: any) { super(message); this.name = 'TokenRefreshError'; } } const XERO_CLIENT_ID = process.env.XERO_CLIENT_ID!; const XERO_CLIENT_SECRET = process.env.XERO_CLIENT_SECRET!; // Secure encryption key handling - fail fast in production let ENCRYPTION_KEY: string; if (process.env.ENCRYPTION_KEY) { ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; } else { if (process.env.NODE_ENV === 'production') { throw new Error('ENCRYPTION_KEY environment variable is required in production'); } console.warn('⚠️ Using insecure default encryption key for development only'); ENCRYPTION_KEY = randomBytes(32).toString('hex'); } // Encryption utilities for sensitive data function encrypt(text: string): string { const iv = randomBytes(16); const cipher = createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } function decrypt(encryptedText: string): string { const [ivHex, encrypted] = encryptedText.split(':'); const iv = Buffer.from(ivHex, 'hex'); const decipher = createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } // Initialize Xero client with proper scopes export function createXeroClient() { return new XeroClient({ clientId: XERO_CLIENT_ID, clientSecret: XERO_CLIENT_SECRET, grantType: 'authorization_code', scopes: [ 'openid', 'profile', 'email', 'accounting.transactions', 'accounting.contacts', 'accounting.settings' ] }); } // Token management functions export async function storeTokens(accountId: string, tokens: any, tenantId?: string) { const encryptedAccessToken = encrypt(tokens.access_token); const encryptedRefreshToken = encrypt(tokens.refresh_token); const expiresAt = new Date(Date.now() + tokens.expires_in * 1000); await db.oAuthToken.create({ data: { accessToken: encryptedAccessToken, refreshToken: encryptedRefreshToken, expiresAt, tokenType: tokens.token_type, scope: tokens.scope, accountId, tenantId } }); } export async function getValidTokens(accountId: string, tenantId?: string) { const tokenRecord = await db.oAuthToken.findFirst({ where: { accountId, tenantId, expiresAt: { gt: new Date() } }, orderBy: { createdAt: 'desc' } }); if (!tokenRecord) { return null; } let accessToken = decrypt(tokenRecord.accessToken); // Check if token is expired or will expire soon (within 5 minutes) const now = new Date(); const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000); if (tokenRecord.expiresAt <= fiveMinutesFromNow) { try { const refreshToken = decrypt(tokenRecord.refreshToken); const newTokens = await refreshAccessToken(refreshToken); // Update stored tokens const encryptedAccessToken = encrypt(newTokens.access_token); const encryptedRefreshToken = encrypt(newTokens.refresh_token); const expiresAt = new Date(Date.now() + newTokens.expires_in * 1000); await db.oAuthToken.update({ where: { id: tokenRecord.id }, data: { accessToken: encryptedAccessToken, refreshToken: encryptedRefreshToken, expiresAt } }); accessToken = newTokens.access_token; } catch (error) { console.error('Token refresh failed:', error); // Mark token as invalid in database to prevent further attempts await db.oAuthToken.update({ where: { id: tokenRecord.id }, data: { expiresAt: new Date(0) } // Mark as expired }); // Throw proper error instead of silent failure throw new TokenRefreshError('Failed to refresh Xero token', error); } } return { access_token: accessToken, token_type: tokenRecord.tokenType, scope: tokenRecord.scope }; } // Multi-tenant Xero client export class XeroTenantClient { private client: XeroClient; private accountId: string; private tenantId: string; constructor(accountId: string, tenantId: string) { this.client = createXeroClient(); this.accountId = accountId; this.tenantId = tenantId; } async initialize() { try { const tokens = await getValidTokens(this.accountId, this.tenantId); if (!tokens) { throw new Error('No valid tokens found for tenant'); } await this.client.setTokenSet(tokens); await this.client.updateTenants(); } catch (error) { if (error instanceof TokenRefreshError) { // Re-throw token refresh errors with context throw new Error(`Token refresh failed for tenant ${this.tenantId}: ${error.message}`); } throw error; } } async getInvoices() { try { const response = await this.client.accountingApi.getInvoices(this.tenantId); return response.body.invoices; } catch (error) { console.error('Error fetching invoices:', error); throw error; } } async getContacts() { try { const response = await this.client.accountingApi.getContacts(this.tenantId); return response.body.contacts; } catch (error) { console.error('Error fetching contacts:', error); throw error; } } async getAccounts() { try { const response = await this.client.accountingApi.getAccounts(this.tenantId); return response.body.accounts; } catch (error) { console.error('Error fetching accounts:', error); throw error; } } // Add more Xero API methods as needed async getItems() { try { const response = await this.client.accountingApi.getItems(this.tenantId); return response.body.items; } catch (error) { console.error('Error fetching items:', error); throw error; } } async getPayments() { try { const response = await this.client.accountingApi.getPayments(this.tenantId); return response.body.payments; } catch (error) { console.error('Error fetching payments:', error); throw error; } } async getBankTransactions() { try { const response = await this.client.accountingApi.getBankTransactions(this.tenantId); return response.body.bankTransactions; } catch (error) { console.error('Error fetching bank transactions:', error); throw error; } } async createContact(contactData: any) { try { const response = await this.client.accountingApi.createContact(this.tenantId, contactData); return response; } catch (error) { console.error('Error creating contact:', error); throw error; } } async createInvoice(invoiceData: any) { try { const response = await this.client.accountingApi.createInvoice(this.tenantId, invoiceData); return response; } catch (error) { console.error('Error creating invoice:', error); throw error; } } async updateContact(contactData: any) { try { const response = await this.client.accountingApi.updateContact(this.tenantId, contactData); return response; } catch (error) { console.error('Error updating contact:', error); throw error; } } } // Factory function to create tenant-specific client export async function createXeroTenantClient(accountId: string, tenantId: string) { const client = new XeroTenantClient(accountId, tenantId); await client.initialize(); return client; } // Store Xero tenant connections export async function storeXeroConnection(accountId: string, tenant: any) { await db.xeroConnection.upsert({ where: { tenantId: tenant.tenantId }, update: { tenantName: tenant.tenantName, tenantType: tenant.tenantType }, create: { tenantId: tenant.tenantId, tenantName: tenant.tenantName, tenantType: tenant.tenantType, accountId } }); } // Get all connected tenants for an account export async function getConnectedTenants(accountId: string) { return await db.xeroConnection.findMany({ where: { accountId }, include: { oauthTokens: { where: { expiresAt: { gt: new Date() } } } } }); }

Latest Blog Posts

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/hiltonbrown/xero-mcp-with-next-js'

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