Jake Shore d3382ec35a Update 6 MCP servers — fieldedge, lightspeed, squarespace, toast, touchbistro, servicetitan — 2026-02-12
=== UPDATES ===
- fieldedge: Added apps, tools, main server entry, full rebuild
- lightspeed: Added complete src/ directory with tools + apps
- squarespace: Full rebuild — new apps, clients, tools, types modules
- toast: Full rebuild — api-client, apps, tools, types
- touchbistro: Full rebuild — api-client, tools, types, gitignore
- servicetitan: Added 4 React UI apps (call-tracking, lead-source-analytics, performance-metrics, schedule-calendar)

All servers restructured from single-file to modular architecture.
2026-02-12 17:58:15 -05:00

236 lines
5.4 KiB
TypeScript

/**
* TouchBistro API Client
* Handles authentication, pagination, error handling, and rate limiting
*/
import type { TouchBistroConfig, PaginatedResponse, PaginationParams } from './types.js';
export class TouchBistroAPIError extends Error {
constructor(
message: string,
public statusCode?: number,
public responseBody?: unknown
) {
super(message);
this.name = 'TouchBistroAPIError';
}
}
export class TouchBistroAPIClient {
private apiKey: string;
private baseUrl: string;
private venueId?: string;
constructor(config: TouchBistroConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.touchbistro.com/v1';
this.venueId = config.venueId;
}
/**
* Make an authenticated request to the TouchBistro API
*/
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers: HeadersInit = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers,
};
if (this.venueId) {
headers['X-Venue-ID'] = this.venueId;
}
try {
const response = await fetch(url, {
...options,
headers,
});
// Handle rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new TouchBistroAPIError(
`Rate limited. Retry after ${retryAfter} seconds`,
429,
{ retryAfter }
);
}
// Parse response body
const contentType = response.headers.get('content-type');
let data: unknown;
if (contentType?.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// Handle errors
if (!response.ok) {
throw new TouchBistroAPIError(
`TouchBistro API error: ${response.statusText}`,
response.status,
data
);
}
return data as T;
} catch (error) {
if (error instanceof TouchBistroAPIError) {
throw error;
}
if (error instanceof Error) {
throw new TouchBistroAPIError(
`Network error: ${error.message}`,
undefined,
error
);
}
throw new TouchBistroAPIError('Unknown error occurred');
}
}
/**
* GET request
*/
async get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T> {
const queryString = params
? '?' + new URLSearchParams(
Object.entries(params)
.filter(([_, v]) => v !== undefined && v !== null)
.map(([k, v]) => [k, String(v)])
).toString()
: '';
return this.request<T>(`${endpoint}${queryString}`, {
method: 'GET',
});
}
/**
* POST request
*/
async post<T>(endpoint: string, body?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
});
}
/**
* PUT request
*/
async put<T>(endpoint: string, body?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
});
}
/**
* PATCH request
*/
async patch<T>(endpoint: string, body?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PATCH',
body: body ? JSON.stringify(body) : undefined,
});
}
/**
* DELETE request
*/
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, {
method: 'DELETE',
});
}
/**
* Paginated GET request - automatically handles pagination
*/
async getPaginated<T>(
endpoint: string,
params: PaginationParams & Record<string, unknown> = {}
): Promise<PaginatedResponse<T>> {
const { page = 1, limit = 50, ...otherParams } = params;
const response = await this.get<{
data: T[];
pagination?: {
total?: number;
page?: number;
limit?: number;
hasMore?: boolean;
nextPage?: string;
};
}>(endpoint, {
page,
limit,
...otherParams,
});
// Normalize pagination response
return {
data: response.data || [],
pagination: {
total: response.pagination?.total || response.data?.length || 0,
page: response.pagination?.page || page,
limit: response.pagination?.limit || limit,
hasMore: response.pagination?.hasMore || false,
},
};
}
/**
* Fetch all pages automatically
*/
async getAllPages<T>(
endpoint: string,
params: Record<string, unknown> = {},
maxPages = 10
): Promise<T[]> {
const allData: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore && page <= maxPages) {
const response = await this.getPaginated<T>(endpoint, {
...params,
page,
limit: 100,
});
allData.push(...response.data);
hasMore = response.pagination.hasMore;
page++;
}
return allData;
}
/**
* Health check
*/
async healthCheck(): Promise<{ status: string; timestamp: string }> {
try {
return await this.get('/health');
} catch (error) {
return {
status: 'error',
timestamp: new Date().toISOString(),
};
}
}
}