Skip to main content
Glama

mcp-google-sheets

index.ts19 kB
import { AuthenticationType, HttpMethod, HttpRequest, httpClient, } from '@activepieces/pieces-common'; import { DropdownState, DynamicPropsValue, Property, } from '@activepieces/pieces-framework'; import Airtable from 'airtable'; import { AirtableBase, AirtableComment, AirtableCreateBaseResponse, AirtableTableConfig, AirtableFieldConfig, AirtableEnterpriseFields, AirtableField, AirtableFieldMapping, AirtableRecord, AirtableTable, AirtableView, } from './models'; import { isNil } from '@activepieces/shared'; interface Params { personalToken: string; baseId: string; tableId: string; fields?: Record<string, unknown>; recordId?: string; searchValue?: string; searchField?: string; fieldNames?: string[]; limitToView?: string; sortField?: string; text?: string; parentCommentId?: string; workspaceId?: string; name?: string; tables?: AirtableTableConfig[]; } async function fetchAllBases({ token, }: { token: string; }): Promise<AirtableBase[]> { const allBases: AirtableBase[] = []; let offset: string | undefined = undefined; do { const request: HttpRequest = { method: HttpMethod.GET, url: 'https://api.airtable.com/v0/meta/bases', authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, queryParams: offset ? { offset } : {}, }; const response = await httpClient.sendRequest<{ bases: AirtableBase[]; offset?: string; }>(request); if (response.status === 200) { allBases.push(...response.body.bases); offset = response.body.offset; } else { offset = undefined; } } while (offset); return allBases; } async function fetchBase({ token, baseId, }: { token: string; baseId: string; }): Promise<AirtableBase> { const request: HttpRequest = { method: HttpMethod.GET, url: `https://api.airtable.com/v0/meta/bases/${baseId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, }; const response = await httpClient.sendRequest<AirtableBase>(request); return response.body; } async function listRecords({ token, baseId, tableId, pageSize = 50, }: { token: string; baseId: string; tableId: string; pageSize?: number; }): Promise<AirtableRecord[]> { const response = await httpClient.sendRequest<{ records: AirtableRecord[] }>({ method: HttpMethod.GET, url: `https://api.airtable.com/v0/${baseId}/${tableId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, queryParams: { pageSize: pageSize.toString(), }, }); if (response.status === 200) { return response.body.records; } return []; } async function fetchTableList({ token, baseId, }: { token: string; baseId: string; }): Promise<AirtableTable[]> { const response = await httpClient.sendRequest<{ tables: AirtableTable[] }>({ method: HttpMethod.GET, url: `https://api.airtable.com/v0/meta/bases/${baseId}/tables`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, }); if (response.status === 200) { return response.body.tables; } return []; } async function fetchTable({ token, baseId, tableId, }: { token: string; baseId: string; tableId: string; }): Promise<AirtableTable> { const tables = await fetchTableList({ token, baseId }); return tables.find((t) => t.id === tableId)!; } async function fetchViews({ token, baseId, tableId, }: { token: string; baseId: string; tableId: string; }): Promise<AirtableView[]> { const table = await fetchTable({ token, baseId, tableId }); if (table) { return table.views; } return []; } async function createRecord({ personalToken: token, fields, tableId, baseId, }: Params) { const request: HttpRequest = { method: HttpMethod.POST, url: `https://api.airtable.com/v0/${baseId}/${tableId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, body: { fields, typecast: true, }, }; const response = await httpClient.sendRequest<AirtableRecord>(request); if (response.status === 200) { return response.body; } return response; } async function findRecord({ personalToken: token, searchField, searchValue, tableId, baseId, limitToView, }: Params) { const request: HttpRequest = { method: HttpMethod.GET, url: `https://api.airtable.com/v0/${baseId}/${tableId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, queryParams: { filterByFormula: `FIND("${searchValue}",{${searchField}})`, view: limitToView ?? '', }, }; const response = await httpClient.sendRequest<{ records: AirtableRecord[]; }>(request); if (response.status === 200) { return response.body.records; } return []; } async function updateRecord({ personalToken: token, fields, recordId, tableId, baseId, }: Params) { const request: HttpRequest = { method: HttpMethod.PATCH, url: `https://api.airtable.com/v0/${baseId}/${tableId}/${recordId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, body: { fields, }, }; const response = await httpClient.sendRequest<AirtableRecord>(request); if (response.status === 200) { return response.body; } return response; } async function deleteRecord({ personalToken: token, recordId, tableId, baseId, }: Params) { const request: HttpRequest = { method: HttpMethod.DELETE, url: `https://api.airtable.com/v0/${baseId}/${tableId}/${recordId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, }; const response = await httpClient.sendRequest<AirtableRecord>(request); if (response.status === 200) { return response.body; } return response; } async function getRecordById({ personalToken: token, baseId, tableId, recordId, }: Params) { const request: HttpRequest = { method: HttpMethod.GET, url: `https://api.airtable.com/v0/${baseId}/${tableId}/${recordId}`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, }; const response = await httpClient.sendRequest<AirtableRecord>(request); if (response.status === 200) { return response.body; } return response; } async function addCommentToRecord({ personalToken: token, baseId, tableId, recordId, text, parentCommentId, }: Params) { const body: { text: string; parentCommentId?: string } = { text: text as string, }; if (parentCommentId) { body.parentCommentId = parentCommentId; } const request: HttpRequest = { method: HttpMethod.POST, url: `https://api.airtable.com/v0/${baseId}/${tableId}/${recordId}/comments`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, body: body, }; const response = await httpClient.sendRequest<AirtableComment>(request); if (response.status === 200 || response.status === 201) { return response.body; } return response; } async function createBase({ personalToken: token, workspaceId, name, tables, }: { personalToken: string; workspaceId: string; name: string; tables: AirtableTableConfig[]; }) { const request: HttpRequest = { method: HttpMethod.POST, url: `https://api.airtable.com/v0/meta/bases`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, body: { name, workspaceId, tables, }, }; const response = await httpClient.sendRequest<AirtableCreateBaseResponse>(request); if (response.status === 200 || response.status === 201) { return response.body; } return response; } async function createTable({ personalToken: token, baseId, name, description, fields, }: { personalToken: string; baseId: string; name: string; description?: string; fields: AirtableFieldConfig[]; }) { const body: { name: string; description?: string; fields: AirtableFieldConfig[]; } = { name, fields }; if (description) { body.description = description; } const request: HttpRequest = { method: HttpMethod.POST, url: `https://api.airtable.com/v0/meta/bases/${baseId}/tables`, authentication: { type: AuthenticationType.BEARER_TOKEN, token, }, body: body, }; const response = await httpClient.sendRequest<AirtableTable>(request); if (response.status === 200 || response.status === 201) { return response.body; } return response; } export const airtableCommon = { base: Property.Dropdown({ displayName: 'Base', required: true, refreshers: [], options: async ({ auth }): Promise<DropdownState<string>> => { if (!auth) { return { disabled: true, options: [], placeholder: 'Please connect your account', }; } try { const bases: AirtableBase[] = await fetchAllBases({ token: auth as string, }); return { disabled: false, options: bases.map((base: AirtableBase) => { return { value: base.id, label: base.name }; }), }; } catch (e) { console.debug(e); return { disabled: true, options: [], placeholder: 'Please check your permission scope', }; } }, }), workspaceId: Property.Dropdown<string>({ displayName: 'Workspace', required: true, refreshers: [], options: async ({ auth }) => { if (!auth) { return { disabled: true, options: [], placeholder: 'Please connect your account', }; } // Although there is no direct way to get a list of workspaces, // we can get a list of bases and then fetch each base to get the workspace id. const bases = await fetchAllBases({ token: auth as string, }); const workspacePromises = bases.map((base) => fetchBase({ token: auth as string, baseId: base.id }) ); const basesWithWorkspaces = await Promise.all(workspacePromises); const workspaces = basesWithWorkspaces.reduce((acc, base) => { if (base.workspaceId) { // Since we don't have the workspace name, we will use the ID as the name. acc[base.workspaceId] = base.workspaceId; } return acc; }, {} as Record<string, string>); return { disabled: false, options: Object.entries(workspaces).map(([id, name]) => ({ label: name, value: id, })), }; }, }), tableId: Property.Dropdown<string>({ displayName: 'Table', required: true, refreshers: ['base'], options: async ({ auth, base }): Promise<DropdownState<string>> => { if (!auth) { return { disabled: true, options: [], placeholder: 'Please connect your account', }; } if (!base) { return { disabled: true, options: [], placeholder: 'Please select a base first', }; } try { const tables: AirtableTable[] = await fetchTableList({ token: auth as string, baseId: base as string, }); return { disabled: false, options: tables.map((table) => ({ value: table.id, label: table.name, })), }; } catch (e) { console.debug(e); return { disabled: true, options: [], placeholder: 'Please check your permission scope', }; } }, }), views: Property.Dropdown<string>({ displayName: 'View', required: false, refreshers: ['base', 'tableId'], options: async ({ auth, base, tableId }): Promise<DropdownState<string>> => { if (!auth) { return { disabled: true, options: [], placeholder: 'Please connect your account', }; } if (!base) { return { disabled: true, options: [], placeholder: 'Please select a base first', }; } if (!tableId) { return { disabled: true, options: [], placeholder: 'Please select a table first', }; } const views: AirtableView[] = await fetchViews({ token: auth as string, baseId: base as string, tableId: tableId as string, }); return { disabled: false, options: views.map((view) => ({ value: view.id, label: view.name, })), }; }, }), recordId: Property.ShortText({ displayName: 'Record ID', required: true, description: 'The ID of the record.', }), fields: Property.DynamicProperties({ displayName: 'Table', required: true, refreshers: ['base', 'tableId'], props: async ({ auth, base, tableId }) => { if (!auth) return {}; if (!base) return {}; if (!tableId) return {}; const airtable: AirtableTable = await fetchTable({ token: auth as unknown as string, baseId: base as unknown as string, tableId: tableId as unknown as string, }); const fields = airtable.fields.reduce((acc, field) => { if (!AirtableEnterpriseFields.includes(field.type)) { const params = { displayName: field.name, description: ['date', 'dateTime'].includes(field.type) ? `${ field.description ? field.description : '' }Expected format: mmmm d,yyyy` : field.description, required: false, }; if (isNil(AirtableFieldMapping[field.type])) { acc[field.id] = Property.ShortText({ ...params, }); } else if ( field.type === 'singleSelect' || field.type === 'multipleSelects' ) { const options = field.options?.choices.map( (option: { id: string; name: string }) => ({ value: option.id, label: option.name, }) ); acc[field.id] = AirtableFieldMapping[field.type]({ ...params, options: { options: options ?? [], }, }); } else { acc[field.id] = AirtableFieldMapping[field.type](params); } } return acc; }, {} as DynamicPropsValue); return fields; }, }), recordIdDropdown: Property.Dropdown<string>({ displayName: 'Record', required: true, refreshers: ['base', 'tableId'], options: async ({ auth, base, tableId }) => { if (!auth || !base || !tableId) { return { disabled: true, options: [], placeholder: 'Please select a base and table first', }; } const table = await fetchTable({ token: auth as string, baseId: base as string, tableId: tableId as string, }); const primaryField = table.fields.find( (f) => f.id === table.primaryFieldId ); const primaryFieldName = primaryField?.name; const records = await listRecords({ token: auth as string, baseId: base as string, tableId: tableId as string, }); const options = records.map((record) => { let label = record.id; if (primaryFieldName && record.fields[primaryFieldName]) { label = record.fields[primaryFieldName] as string; } return { label: label, value: record.id, }; }); return { disabled: false, options: options, }; }, }), fieldNames: Property.Dropdown({ displayName: 'Search Field', required: true, refreshers: ['base', 'tableId'], options: async ({ auth, base, tableId }): Promise<DropdownState<string>> => { if (!auth) { return { disabled: true, options: [], placeholder: 'Please connect your account', }; } if (!base) { return { disabled: true, options: [], placeholder: 'Please select a base first', }; } if (!tableId) { return { disabled: true, options: [], placeholder: 'Please select a table first', }; } const airtable: AirtableTable = await fetchTable({ token: auth as unknown as string, baseId: base as unknown as string, tableId: tableId as unknown as string, }); return { disabled: false, options: airtable.fields.map((field: AirtableField) => ({ label: field.name, value: field.name, })), }; }, }), createNewFields: async ( auth: string, base: string, tableId: string, fields: Record<string, unknown> ) => { if (!auth) return fields; if (!base) return fields; if (!tableId) return fields; const newFields: Record<string, unknown> = {}; const airtable: AirtableTable = await fetchTable({ token: auth, baseId: base, tableId: tableId, }); airtable.fields.forEach((field) => { if (!AirtableEnterpriseFields.includes(field.type)) { const key = field.id; if (field.type === 'multipleAttachments' && fields[key]) { newFields[key] = [ { url: fields[key] as string, }, ]; } else if ( ['multipleRecordLinks', 'multipleSelects'].includes(field.type) ) { if (Array.isArray(fields[key]) && (fields[key] as any[]).length > 0) { newFields[key] = fields[key]; } } else { newFields[key] = fields[key]; } } }); return newFields; }, getTableSnapshot: async (params: Params) => { Airtable.configure({ apiKey: params.personalToken, }); const airtable = new Airtable(); const currentTableSnapshot = ( await airtable .base(params.baseId) .table(params.tableId) .select(params.limitToView ? { view: params.limitToView } : {}) .all() ) .map((r) => r._rawJson) .sort( (x, y) => new Date(x.createdTime).getTime() - new Date(y.createdTime).getTime() ); return currentTableSnapshot; }, createRecord, findRecord, updateRecord, deleteRecord, getRecordById, fetchAllBases, fetchTableList, fetchTable, fetchViews, addCommentToRecord, createBase, createTable, listRecords, fetchBase, };

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/activepieces/activepieces'

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