Skip to main content
Glama
LawrenceCirillo

QuickBase MCP Server

client.ts16.4 kB
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { QuickBaseConfig, QuickBaseField, QuickBaseTable, QuickBaseRecord, QueryOptions } from '../types/quickbase.js'; export class QuickBaseClient { private axios: AxiosInstance; private config: QuickBaseConfig; constructor(config: QuickBaseConfig) { this.config = config; this.axios = axios.create({ baseURL: `https://api.quickbase.com/v1`, timeout: config.timeout, headers: { 'QB-Realm-Hostname': config.realm, 'User-Agent': 'QuickBase-MCP-Server/1.0.0', 'Authorization': `QB-USER-TOKEN ${config.userToken}`, 'Content-Type': 'application/json' } }); // Add request/response interceptors for logging and error handling this.axios.interceptors.request.use( (config) => { console.log(`QB API Request: ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => Promise.reject(error) ); this.axios.interceptors.response.use( (response) => { console.log(`QB API Response: ${response.status} ${response.config.url}`); return response; }, (error) => { console.error(`QB API Error: ${error.response?.status} ${error.response?.data?.message || error.message}`); return Promise.reject(error); } ); } // ========== APPLICATION METHODS ========== async getAppInfo(): Promise<any> { const response = await this.axios.get(`/apps/${this.config.appId}`); return response.data; } async getAppTables(): Promise<any[]> { const response = await this.axios.get(`/tables`, { params: { appId: this.config.appId } }); return response.data; } // ========== TABLE METHODS ========== async createTable(table: { name: string; description?: string }): Promise<string> { const response = await this.axios.post('/tables', { appId: this.config.appId, name: table.name, description: table.description, singleRecordName: table.name.slice(0, -1), // Remove 's' for singular pluralRecordName: table.name }); return response.data.id; } async getTableInfo(tableId: string): Promise<any> { const response = await this.axios.get(`/tables/${tableId}`, { params: { appId: this.config.appId } }); return response.data; } async updateTable(tableId: string, updates: Partial<QuickBaseTable>): Promise<void> { await this.axios.post(`/tables/${tableId}`, { appId: this.config.appId, ...updates }); } async deleteTable(tableId: string): Promise<void> { await this.axios.delete(`/tables/${tableId}`, { params: { appId: this.config.appId } }); } // ========== FIELD METHODS ========== async getTableFields(tableId: string): Promise<any[]> { const response = await this.axios.get(`/fields`, { params: { tableId } }); return response.data; } async createField(tableId: string, field: QuickBaseField): Promise<number> { const fieldData: any = { tableId, label: field.label, fieldType: field.fieldType, required: field.required, unique: field.unique }; // Add field-specific properties if (field.choices && ['text_choice', 'multiselect'].includes(field.fieldType)) { fieldData.properties = { choices: field.choices }; } if (field.formula && field.fieldType === 'formula') { fieldData.formula = field.formula; } if (field.lookupReference && field.fieldType === 'lookup') { fieldData.properties = { lookupReference: field.lookupReference }; } const response = await this.axios.post('/fields', fieldData); return response.data.id; } async updateField(tableId: string, fieldId: number, updates: Partial<QuickBaseField>): Promise<void> { await this.axios.post(`/fields/${fieldId}`, { tableId, ...updates }); } async deleteField(tableId: string, fieldId: number): Promise<void> { await this.axios.delete(`/fields/${fieldId}`, { params: { tableId } }); } // ========== RECORD METHODS ========== async getRecords(tableId: string, options?: QueryOptions): Promise<any[]> { const params: any = { from: tableId }; if (options?.select) { params.select = options.select; } if (options?.where) { params.where = options.where; } if (options?.sortBy) { params.sortBy = options.sortBy; } if (options?.groupBy) { params.groupBy = options.groupBy; } if (options?.top) { params.top = options.top; } if (options?.skip) { params.skip = options.skip; } const response = await this.axios.post('/records/query', params); return response.data.data; } async getRecord(tableId: string, recordId: number, fieldIds?: number[]): Promise<any> { const params: any = { from: tableId }; if (fieldIds) { params.select = fieldIds; } const response = await this.axios.post('/records/query', { ...params, where: `{3.EX.${recordId}}` }); return response.data.data[0] || null; } async createRecord(tableId: string, record: QuickBaseRecord): Promise<number> { const response = await this.axios.post('/records', { to: tableId, data: [{ ...record.fields }] }); return response.data.data[0]['3'].value; // Record ID is always field 3 } async createRecords(tableId: string, records: QuickBaseRecord[]): Promise<number[]> { const response = await this.axios.post('/records', { to: tableId, data: records.map(record => record.fields) }); return response.data.data.map((record: any) => record['3'].value); } async updateRecord(tableId: string, recordId: number, updates: Record<string, any>): Promise<void> { await this.axios.post('/records', { to: tableId, data: [{ '3': { value: recordId }, // Record ID field ...updates }] }); } async updateRecords(tableId: string, records: Array<{ recordId: number; updates: Record<string, any> }>): Promise<void> { await this.axios.post('/records', { to: tableId, data: records.map(({ recordId, updates }) => ({ '3': { value: recordId }, ...updates })) }); } async deleteRecord(tableId: string, recordId: number): Promise<void> { await this.axios.delete('/records', { data: { from: tableId, where: `{3.EX.${recordId}}` } }); } async deleteRecords(tableId: string, recordIds: number[]): Promise<void> { const whereClause = recordIds.map(id => `{3.EX.${id}}`).join('OR'); await this.axios.delete('/records', { data: { from: tableId, where: whereClause } }); } // ========== RELATIONSHIP METHODS ========== async createRelationship(parentTableId: string, childTableId: string, foreignKeyFieldId: number): Promise<void> { await this.axios.post('/relationships', { parentTableId, childTableId, foreignKeyFieldId }); } async getRelationships(tableId: string): Promise<any[]> { const response = await this.axios.get(`/relationships`, { params: { childTableId: tableId } }); return response.data; } // ========== ENHANCED RELATIONSHIP METHODS ========== async createAdvancedRelationship( parentTableId: string, childTableId: string, referenceFieldLabel: string, lookupFields?: Array<{ parentFieldId: number; childFieldLabel: string }>, relationshipType: 'one-to-many' | 'many-to-many' = 'one-to-many' ): Promise<{ referenceFieldId: number; lookupFieldIds: number[] }> { try { // Step 1: Create the reference field in the child table const referenceFieldId = await this.createField(childTableId, { label: referenceFieldLabel, fieldType: 'reference', required: false, unique: false, properties: { lookupTableId: parentTableId } }); // Step 2: Create the relationship await this.createRelationship(parentTableId, childTableId, referenceFieldId); // Step 3: Create lookup fields if specified const lookupFieldIds: number[] = []; if (lookupFields && lookupFields.length > 0) { for (const lookup of lookupFields) { const lookupFieldId = await this.createLookupField( childTableId, parentTableId, referenceFieldId, lookup.parentFieldId, lookup.childFieldLabel ); lookupFieldIds.push(lookupFieldId); } } return { referenceFieldId, lookupFieldIds }; } catch (error) { console.error('Error creating advanced relationship:', error); throw error; } } async createLookupField( childTableId: string, parentTableId: string, referenceFieldId: number, parentFieldId: number, lookupFieldLabel: string ): Promise<number> { const response = await this.axios.post('/fields', { tableId: childTableId, label: lookupFieldLabel, fieldType: 'lookup', properties: { lookupReference: { tableId: parentTableId, fieldId: parentFieldId, referenceFieldId: referenceFieldId } } }); return response.data.id; } async validateRelationship( parentTableId: string, childTableId: string, foreignKeyFieldId: number ): Promise<{ isValid: boolean; issues: string[]; orphanedRecords: number }> { const issues: string[] = []; let orphanedRecords = 0; try { // Check if parent table exists await this.getTableInfo(parentTableId); } catch (error) { issues.push(`Parent table ${parentTableId} not found`); } try { // Check if child table exists await this.getTableInfo(childTableId); } catch (error) { issues.push(`Child table ${childTableId} not found`); } try { // Check if foreign key field exists const childFields = await this.getTableFields(childTableId); const foreignKeyField = childFields.find(field => field.id === foreignKeyFieldId); if (!foreignKeyField) { issues.push(`Foreign key field ${foreignKeyFieldId} not found in child table`); } else if (foreignKeyField.fieldType !== 'reference') { issues.push(`Field ${foreignKeyFieldId} is not a reference field`); } // Check for orphaned records (child records with invalid parent references) const childRecords = await this.getRecords(childTableId, { select: [3, foreignKeyFieldId], // Record ID and foreign key where: `{${foreignKeyFieldId}.XEX.''}` }); for (const record of childRecords) { const foreignKeyValue = record[foreignKeyFieldId]?.value; if (foreignKeyValue) { try { await this.getRecord(parentTableId, foreignKeyValue); } catch (error) { orphanedRecords++; } } } } catch (error) { issues.push(`Error validating relationship: ${error instanceof Error ? error.message : 'Unknown error'}`); } return { isValid: issues.length === 0 && orphanedRecords === 0, issues, orphanedRecords }; } async getRelationshipDetails(tableId: string, includeFields: boolean = true): Promise<any> { try { const relationships = await this.getRelationships(tableId); const tableInfo = await this.getTableInfo(tableId); const result = { tableId, tableName: tableInfo.name, relationships: [] as any[], relatedFields: [] as any[] }; for (const relationship of relationships) { const relationshipDetail: any = { parentTableId: relationship.parentTableId, childTableId: relationship.childTableId, foreignKeyFieldId: relationship.foreignKeyFieldId, type: 'one-to-many' // QuickBase primarily supports one-to-many }; if (includeFields) { // Get fields related to this relationship const fields = await this.getTableFields(tableId); const relatedFields = fields.filter(field => field.fieldType === 'reference' || field.fieldType === 'lookup' || (field.properties && field.properties.lookupReference) ); relationshipDetail.relatedFields = relatedFields; } result.relationships.push(relationshipDetail); } return result; } catch (error) { console.error('Error getting relationship details:', error); throw error; } } async createJunctionTable( junctionTableName: string, table1Id: string, table2Id: string, table1FieldLabel: string, table2FieldLabel: string, additionalFields?: Array<{ label: string; fieldType: string }> ): Promise<{ junctionTableId: string; table1ReferenceFieldId: number; table2ReferenceFieldId: number }> { try { // Step 1: Create the junction table const junctionTableId = await this.createTable({ name: junctionTableName, description: `Junction table for many-to-many relationship between tables ${table1Id} and ${table2Id}` }); // Step 2: Create reference field to first table const table1ReferenceFieldId = await this.createField(junctionTableId, { label: table1FieldLabel, fieldType: 'reference', required: true, unique: false, properties: { lookupTableId: table1Id } }); // Step 3: Create reference field to second table const table2ReferenceFieldId = await this.createField(junctionTableId, { label: table2FieldLabel, fieldType: 'reference', required: true, unique: false, properties: { lookupTableId: table2Id } }); // Step 4: Create relationships await this.createRelationship(table1Id, junctionTableId, table1ReferenceFieldId); await this.createRelationship(table2Id, junctionTableId, table2ReferenceFieldId); // Step 5: Create additional fields if specified if (additionalFields && additionalFields.length > 0) { for (const field of additionalFields) { await this.createField(junctionTableId, { label: field.label, fieldType: field.fieldType as any, required: false, unique: false }); } } return { junctionTableId, table1ReferenceFieldId, table2ReferenceFieldId }; } catch (error) { console.error('Error creating junction table:', error); throw error; } } // ========== REPORT METHODS ========== async getReports(tableId: string): Promise<any[]> { const response = await this.axios.get('/reports', { params: { tableId } }); return response.data; } async runReport(reportId: string, tableId: string): Promise<any[]> { const response = await this.axios.post('/records/query', { from: tableId, options: { reportId } }); return response.data.data; } // ========== UTILITY METHODS ========== async testConnection(): Promise<boolean> { try { await this.getAppInfo(); return true; } catch (error) { return false; } } async searchRecords(tableId: string, searchTerm: string, fieldIds?: number[]): Promise<any[]> { // This is a simple implementation - you might want to enhance based on your search needs const whereClause = fieldIds ? fieldIds.map(fieldId => `{${fieldId}.CT.'${searchTerm}'}`).join('OR') : `{6.CT.'${searchTerm}'}OR{7.CT.'${searchTerm}'}`; // Common text fields return this.getRecords(tableId, { where: whereClause }); } // ========== BULK OPERATIONS ========== async upsertRecords(tableId: string, records: Array<{ keyField: number; keyValue: any; data: Record<string, any> }>): Promise<any> { // QuickBase upsert based on a key field return await this.axios.post('/records', { to: tableId, data: records.map(({ keyField, keyValue, data }) => ({ [keyField]: { value: keyValue }, ...data })), mergeFieldId: records[0]?.keyField }); } }

Implementation Reference

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/LawrenceCirillo/QuickBase-MCP-Server'

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