Skip to main content
Glama

mcp-google-sheets

props-resolver.ts8.8 kB
import { applyFunctionToValues, isNil, isString } from '@activepieces/shared' import replaceAsync from 'string-replace-async' import { initCodeSandbox } from '../core/code/code-sandbox' import { FlowExecutorContext } from '../handler/context/flow-execution-context' import { createConnectionService } from '../services/connections.service' const VARIABLE_PATTERN = /\{\{(.*?)\}\}/g const CONNECTIONS = 'connections' const FLATTEN_NESTED_KEYS_PATTERN = /\{\{\s*flattenNestedKeys(.*?)\}\}/g type PropsResolverParams = { engineToken: string projectId: string apiUrl: string } export const createPropsResolver = ({ engineToken, projectId, apiUrl }: PropsResolverParams) => { return { resolve: async <T = unknown>(params: ResolveInputParams): Promise<ResolveResult<T>> => { const { unresolvedInput, executionState } = params if (isNil(unresolvedInput)) { return { resolvedInput: unresolvedInput as T, censoredInput: unresolvedInput, } } const currentState = executionState.currentState() const resolveOptions = { engineToken, projectId, apiUrl, currentState, } const resolvedInput = await applyFunctionToValues<T>( unresolvedInput, (token) => resolveInputAsync({ ...resolveOptions, input: token, censoredInput: false, })) const censoredInput = await applyFunctionToValues<T>( unresolvedInput, (token) => resolveInputAsync({ ...resolveOptions, input: token, censoredInput: true, })) return { resolvedInput, censoredInput, } }, } } const mergeFlattenedKeysArraysIntoOneArray = async (token: string, partsThatNeedResolving: string[], resolveOptions: Pick<ResolveInputInternalParams, 'engineToken' | 'projectId' | 'apiUrl' | 'currentState' | 'censoredInput'>) => { const resolvedValues: Record<string, unknown> = {} let longestResultLength = 0 for (const tokenPart of partsThatNeedResolving) { const variableName = tokenPart.substring(2, tokenPart.length - 2) resolvedValues[tokenPart] = await resolveSingleToken({ ...resolveOptions, variableName, }) if (Array.isArray(resolvedValues[tokenPart])) { longestResultLength = Math.max(longestResultLength, resolvedValues[tokenPart].length) } } const result = new Array(longestResultLength).fill(null).map((_, index) => { return Object.entries(resolvedValues).reduce((acc, [tokenPart, value]) => { const valueToUse = (Array.isArray(value) ? value[index] : value) ?? '' acc = acc.replace(tokenPart, isString(valueToUse) ? valueToUse : JSON.stringify(valueToUse)) return acc }, token) }) return result } export type PropsResolver = ReturnType<typeof createPropsResolver> /** * input: `Hello {{firstName}} {{lastName}}` * tokenThatNeedResolving: [`{{firstName}}`, `{{lastName}}`] */ async function resolveInputAsync(params: ResolveInputInternalParams): Promise<unknown> { const { input, currentState, engineToken, projectId, apiUrl, censoredInput } = params const tokensThatNeedResolving = input.match(VARIABLE_PATTERN) const inputContainsOnlyOneTokenToResolve = tokensThatNeedResolving !== null && tokensThatNeedResolving.length === 1 && tokensThatNeedResolving[0] === input const resolveOptions = { engineToken, projectId, apiUrl, currentState, censoredInput, } if (inputContainsOnlyOneTokenToResolve) { const trimmedInput = input.trim() const variableName = trimmedInput.substring(2, trimmedInput.length - 2) return resolveSingleToken({ ...resolveOptions, variableName, }) } const inputIncludesFlattenNestedKeysTokens = input.match(FLATTEN_NESTED_KEYS_PATTERN) if (!isNil(inputIncludesFlattenNestedKeysTokens) && !isNil(tokensThatNeedResolving)) { return mergeFlattenedKeysArraysIntoOneArray(input, tokensThatNeedResolving, resolveOptions) } return replaceAsync(input, VARIABLE_PATTERN, async (_fullMatch, variableName) => { const result = await resolveSingleToken({ ...resolveOptions, variableName, }) return isString(result) ? result : JSON.stringify(result) }) } async function resolveSingleToken(params: ResolveSingleTokenParams): Promise<unknown> { const { variableName, currentState } = params const isConnection = variableName.startsWith(CONNECTIONS) if (isConnection) { return handleConnection(params) } return evalInScope(variableName, { ...currentState }, { flattenNestedKeys }) } async function handleConnection(params: ResolveSingleTokenParams): Promise<unknown> { const { variableName, engineToken, projectId, apiUrl, censoredInput } = params const connectionName = parseConnectionNameOnly(variableName) if (isNil(connectionName)) { return '' } if (censoredInput) { return '**REDACTED**' } const connection = await createConnectionService({ engineToken, projectId, apiUrl }).obtain(connectionName) const pathAfterConnectionName = parsePathAfterConnectionName(variableName, connectionName) if (isNil(pathAfterConnectionName) || pathAfterConnectionName.length === 0) { return connection } return evalInScope(pathAfterConnectionName, { connection }, { flattenNestedKeys }) } function parsePathAfterConnectionName(variableName: string, connectionName: string): string | null { if (variableName.includes('[')) { return variableName.substring(`connections.['${connectionName}']`.length) } const cp = variableName.substring(`connections.${connectionName}`.length) if (cp.length === 0) { return cp } return `connection${cp}` } function parseConnectionNameOnly(variableName: string): string | null { const connectionWithNewFormatSquareBrackets = variableName.includes('[') if (connectionWithNewFormatSquareBrackets) { return parseSquareBracketConnectionPath(variableName) } // {{connections.connectionName.path}} // This does not work If connectionName contains . return variableName.split('.')?.[1] } function parseSquareBracketConnectionPath(variableName: string): string | null { // Find the connection name inside {{connections['connectionName'].path}} const matches = variableName.match(/\['([^']+)'\]/g) if (matches && matches.length >= 1) { // Remove the square brackets and quotes from the connection name const secondPath = matches[0].replace(/\['|'\]/g, '') return secondPath } return null } // eslint-disable-next-line @typescript-eslint/ban-types async function evalInScope(js: string, contextAsScope: Record<string, unknown>, functions: Record<string, Function>): Promise<unknown> { try { const codeSandbox = await initCodeSandbox() const result = await codeSandbox.runScript({ script: js, scriptContext: contextAsScope, functions, }) return result ?? '' } catch (exception) { console.warn('[evalInScope] Error evaluating variable', exception) return '' } } function flattenNestedKeys(data: unknown, pathToMatch: string[]): unknown[] { if (typeof data === 'object' && data !== null && !Array.isArray(data)) { for (const [key, value] of Object.entries(data as Record<string, unknown>)) { if (key === pathToMatch[0]) { return flattenNestedKeys(value, pathToMatch.slice(1)) } } } else if (Array.isArray(data)) { return data.flatMap((d) => flattenNestedKeys(d, pathToMatch)) } else if (pathToMatch.length === 0) { return [data] } return [] } type ResolveSingleTokenParams = { variableName: string currentState: Record<string, unknown> engineToken: string projectId: string apiUrl: string censoredInput: boolean } type ResolveInputInternalParams = { input: string engineToken: string projectId: string apiUrl: string censoredInput: boolean currentState: Record<string, unknown> } type ResolveInputParams = { unresolvedInput: unknown executionState: FlowExecutorContext } type ResolveResult<T = unknown> = { resolvedInput: T censoredInput: unknown }

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